From c605d1951031e95f0bc5923ebaa986f8f1057472 Mon Sep 17 00:00:00 2001 From: Cordelia Mist Date: Thu, 16 Jan 2025 19:34:46 -0800 Subject: [PATCH 01/10] Progress made. May be successful! --- Glamourer/Api/StateApi.cs | 2 + Glamourer/Automation/AutoDesignApplier.cs | 12 ++ Glamourer/Interop/ChangeCustomizeService.cs | 7 +- Glamourer/Interop/InventoryService.cs | 82 ++++++++----- Glamourer/Interop/JobService.cs | 2 +- Glamourer/Interop/UpdateSlotService.cs | 124 ++++++++++++++++++-- Glamourer/State/StateEditor.cs | 26 ++-- Glamourer/State/StateListener.cs | 5 +- Glamourer/State/StateManager.cs | 4 +- 9 files changed, 201 insertions(+), 63 deletions(-) diff --git a/Glamourer/Api/StateApi.cs b/Glamourer/Api/StateApi.cs index 331942b..ce7d226 100644 --- a/Glamourer/Api/StateApi.cs +++ b/Glamourer/Api/StateApi.cs @@ -333,6 +333,8 @@ public sealed class StateApi : IGlamourerApiState, IApiService, IDisposable private void OnStateChanged(StateChangeType type, StateSource _2, ActorState _3, ActorData actors, ITransaction? _5) { + Glamourer.Log.Error($"[OnStateChanged API CALL] Sending out OnStateChanged with type {type}."); + if (StateChanged != null) foreach (var actor in actors.Objects) StateChanged.Invoke(actor.Address); diff --git a/Glamourer/Automation/AutoDesignApplier.cs b/Glamourer/Automation/AutoDesignApplier.cs index 660acf4..d49864f 100644 --- a/Glamourer/Automation/AutoDesignApplier.cs +++ b/Glamourer/Automation/AutoDesignApplier.cs @@ -1,4 +1,5 @@ using Dalamud.Plugin.Services; +using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; using FFXIVClientStructs.FFXIV.Client.UI.Misc; using Glamourer.Designs; using Glamourer.Designs.Links; @@ -201,11 +202,22 @@ public sealed class AutoDesignApplier : IDisposable } } + /// + /// JOB CHANGE IS CALLED UPON HERE. + /// private void OnJobChange(Actor actor, Job oldJob, Job newJob) { + unsafe + { + var drawObject = actor.AsCharacter->DrawObject; + Glamourer.Log.Information($"[AutoDesignApplier][OnJobChange] 0x{(nint)drawObject:X} changed job from {oldJob} ({oldJob.Id}) to {newJob} ({newJob.Id})."); + } + if (!_config.EnableAutoDesigns || !actor.Identifier(_actors, out var id)) return; + Glamourer.Log.Information($"[AutoDesignApplier][OnJobChange] We had EnableAutoDesigns active, and are a valid actor!"); + if (!GetPlayerSet(id, out var set)) { if (_state.TryGetValue(id, out var s)) diff --git a/Glamourer/Interop/ChangeCustomizeService.cs b/Glamourer/Interop/ChangeCustomizeService.cs index 495d69c..032412e 100644 --- a/Glamourer/Interop/ChangeCustomizeService.cs +++ b/Glamourer/Interop/ChangeCustomizeService.cs @@ -64,12 +64,13 @@ public unsafe class ChangeCustomizeService : EventWrapperRef2 _changeCustomizeHook; + // manual invoke by calling the detours _original call to `execute to` instead of `listen to`. public bool UpdateCustomize(Model model, CustomizeArray customize) { if (!model.IsHuman) return false; - Glamourer.Log.Verbose($"[ChangeCustomize] Invoked on 0x{model.Address:X} with {customize}."); + Glamourer.Log.Information($"[ChangeCustomize] Glamour-Invoked on 0x{model.Address:X} with {customize}."); using var _ = InUpdate.EnterMethod(); var ret = _original(model.AsHuman, customize.Data, true); return ret; @@ -78,16 +79,20 @@ public unsafe class ChangeCustomizeService : EventWrapperRef2 UpdateCustomize(actor.Model, customize); + // detoured method. private bool ChangeCustomizeDetour(Human* human, byte* data, byte skipEquipment) { if (!InUpdate.InMethod) Invoke(human, ref *(CustomizeArray*)data); var ret = _changeCustomizeHook.Original(human, data, skipEquipment); + + Glamourer.Log.Information($"[ChangeCustomize] Called on with {*(CustomizeArray*)data} ({ret})."); _postEvent.Invoke(human); return ret; } + public void Subscribe(Action action, Post.Priority priority) => _postEvent.Subscribe(action, priority); diff --git a/Glamourer/Interop/InventoryService.cs b/Glamourer/Interop/InventoryService.cs index 6d8e58b..605ca15 100644 --- a/Glamourer/Interop/InventoryService.cs +++ b/Glamourer/Interop/InventoryService.cs @@ -1,9 +1,11 @@ using Dalamud.Hooking; using Dalamud.Plugin.Services; +using Dalamud.Utility.Signatures; using FFXIVClientStructs.FFXIV.Client.Game; using FFXIVClientStructs.FFXIV.Client.UI.Misc; using Glamourer.Events; using OtterGui.Services; +using Penumbra.GameData; using Penumbra.GameData.Enums; using Penumbra.GameData.Structs; using Penumbra.String; @@ -16,39 +18,48 @@ public sealed unsafe class InventoryService : IDisposable, IRequiredService private readonly EquippedGearset _gearsetEvent; private readonly List<(EquipSlot, uint, StainIds)> _itemList = new(12); + // This can be moved into client structs or penumbra.gamedata when needed. + public const string EquipGearsetInternal = "40 55 53 56 57 41 57 48 8D AC 24 ?? ?? ?? ?? 48 81 EC ?? ?? ?? ?? 48 8B 05 ?? ?? ?? ?? 48 33 C4 48 89 85 ?? ?? ?? ?? 4C 63 FA"; + private delegate nint ChangeGearsetInternalDelegate(RaptureGearsetModule* module, uint gearsetId, byte glamourPlateId); + + [Signature(EquipGearsetInternal, DetourName = nameof(EquipGearSetInternalDetour))] + private readonly Hook _equipGearsetInternalHook = null!; + public InventoryService(MovedEquipment movedItemsEvent, IGameInteropProvider interop, EquippedGearset gearsetEvent) { _movedItemsEvent = movedItemsEvent; _gearsetEvent = gearsetEvent; _moveItemHook = interop.HookFromAddress((nint)InventoryManager.MemberFunctionPointers.MoveItemSlot, MoveItemDetour); - _equipGearsetHook = - interop.HookFromAddress((nint)RaptureGearsetModule.MemberFunctionPointers.EquipGearset, EquipGearSetDetour); + _equipGearsetHook = interop.HookFromAddress((nint)RaptureGearsetModule.MemberFunctionPointers.EquipGearset, EquipGearSetDetour); + interop.InitializeFromAttributes(this); _moveItemHook.Enable(); _equipGearsetHook.Enable(); + _equipGearsetInternalHook.Enable(); } public void Dispose() { _moveItemHook.Dispose(); _equipGearsetHook.Dispose(); + _equipGearsetInternalHook.Dispose(); } private delegate int EquipGearsetDelegate(RaptureGearsetModule* module, int gearsetId, byte glamourPlateId); private readonly Hook _equipGearsetHook; - private int EquipGearSetDetour(RaptureGearsetModule* module, int gearsetId, byte glamourPlateId) + private nint EquipGearSetInternalDetour(RaptureGearsetModule* module, uint gearsetId, byte glamourPlateId) { var prior = module->CurrentGearsetIndex; - var ret = _equipGearsetHook.Original(module, gearsetId, glamourPlateId); - var set = module->GetGearset(gearsetId); - _gearsetEvent.Invoke(new ByteString(set->Name).ToString(), gearsetId, prior, glamourPlateId, set->ClassJob); - Glamourer.Log.Excessive($"[InventoryService] Applied gear set {gearsetId} with glamour plate {glamourPlateId} (Returned {ret})"); + var ret = _equipGearsetInternalHook.Original(module, gearsetId, glamourPlateId); + var set = module->GetGearset((int)gearsetId); + _gearsetEvent.Invoke(new ByteString(set->Name).ToString(), (int)gearsetId, prior, glamourPlateId, set->ClassJob); + Glamourer.Log.Warning($"[InventoryService] [EquipInternal] Applied gear set {gearsetId} with glamour plate {glamourPlateId} (Returned {ret})"); if (ret == 0) { - var entry = module->GetGearset(gearsetId); + var entry = module->GetGearset((int)gearsetId); if (entry == null) return ret; @@ -73,18 +84,18 @@ public sealed unsafe class InventoryService : IDisposable, IRequiredService } var plate = MirageManager.Instance()->GlamourPlates[glamourPlateId - 1]; - Add(EquipSlot.MainHand, plate.ItemIds[0], StainIds.FromGlamourPlate(plate, 0), ref entry->Items[0]); - Add(EquipSlot.OffHand, plate.ItemIds[1], StainIds.FromGlamourPlate(plate, 1), ref entry->Items[1]); - Add(EquipSlot.Head, plate.ItemIds[2], StainIds.FromGlamourPlate(plate, 2), ref entry->Items[2]); - Add(EquipSlot.Body, plate.ItemIds[3], StainIds.FromGlamourPlate(plate, 3), ref entry->Items[3]); - Add(EquipSlot.Hands, plate.ItemIds[4], StainIds.FromGlamourPlate(plate, 4), ref entry->Items[5]); - Add(EquipSlot.Legs, plate.ItemIds[5], StainIds.FromGlamourPlate(plate, 5), ref entry->Items[6]); - Add(EquipSlot.Feet, plate.ItemIds[6], StainIds.FromGlamourPlate(plate, 6), ref entry->Items[7]); - Add(EquipSlot.Ears, plate.ItemIds[7], StainIds.FromGlamourPlate(plate, 7), ref entry->Items[8]); - Add(EquipSlot.Neck, plate.ItemIds[8], StainIds.FromGlamourPlate(plate, 8), ref entry->Items[9]); - Add(EquipSlot.Wrists, plate.ItemIds[9], StainIds.FromGlamourPlate(plate, 9), ref entry->Items[10]); - Add(EquipSlot.RFinger, plate.ItemIds[10], StainIds.FromGlamourPlate(plate, 10), ref entry->Items[11]); - Add(EquipSlot.LFinger, plate.ItemIds[11], StainIds.FromGlamourPlate(plate, 11), ref entry->Items[12]); + Add(EquipSlot.MainHand, plate.ItemIds[0], StainIds.FromGlamourPlate(plate, 0), ref entry->Items[0]); + Add(EquipSlot.OffHand, plate.ItemIds[1], StainIds.FromGlamourPlate(plate, 1), ref entry->Items[1]); + Add(EquipSlot.Head, plate.ItemIds[2], StainIds.FromGlamourPlate(plate, 2), ref entry->Items[2]); + Add(EquipSlot.Body, plate.ItemIds[3], StainIds.FromGlamourPlate(plate, 3), ref entry->Items[3]); + Add(EquipSlot.Hands, plate.ItemIds[4], StainIds.FromGlamourPlate(plate, 4), ref entry->Items[5]); + Add(EquipSlot.Legs, plate.ItemIds[5], StainIds.FromGlamourPlate(plate, 5), ref entry->Items[6]); + Add(EquipSlot.Feet, plate.ItemIds[6], StainIds.FromGlamourPlate(plate, 6), ref entry->Items[7]); + Add(EquipSlot.Ears, plate.ItemIds[7], StainIds.FromGlamourPlate(plate, 7), ref entry->Items[8]); + Add(EquipSlot.Neck, plate.ItemIds[8], StainIds.FromGlamourPlate(plate, 8), ref entry->Items[9]); + Add(EquipSlot.Wrists, plate.ItemIds[9], StainIds.FromGlamourPlate(plate, 9), ref entry->Items[10]); + Add(EquipSlot.RFinger, plate.ItemIds[10], StainIds.FromGlamourPlate(plate, 10), ref entry->Items[11]); + Add(EquipSlot.LFinger, plate.ItemIds[11], StainIds.FromGlamourPlate(plate, 11), ref entry->Items[12]); } else { @@ -99,17 +110,17 @@ public sealed unsafe class InventoryService : IDisposable, IRequiredService } Add(EquipSlot.MainHand, ref entry->Items[0]); - Add(EquipSlot.OffHand, ref entry->Items[1]); - Add(EquipSlot.Head, ref entry->Items[2]); - Add(EquipSlot.Body, ref entry->Items[3]); - Add(EquipSlot.Hands, ref entry->Items[5]); - Add(EquipSlot.Legs, ref entry->Items[6]); - Add(EquipSlot.Feet, ref entry->Items[7]); - Add(EquipSlot.Ears, ref entry->Items[8]); - Add(EquipSlot.Neck, ref entry->Items[9]); - Add(EquipSlot.Wrists, ref entry->Items[10]); - Add(EquipSlot.RFinger, ref entry->Items[11]); - Add(EquipSlot.LFinger, ref entry->Items[12]); + Add(EquipSlot.OffHand, ref entry->Items[1]); + Add(EquipSlot.Head, ref entry->Items[2]); + Add(EquipSlot.Body, ref entry->Items[3]); + Add(EquipSlot.Hands, ref entry->Items[5]); + Add(EquipSlot.Legs, ref entry->Items[6]); + Add(EquipSlot.Feet, ref entry->Items[7]); + Add(EquipSlot.Ears, ref entry->Items[8]); + Add(EquipSlot.Neck, ref entry->Items[9]); + Add(EquipSlot.Wrists, ref entry->Items[10]); + Add(EquipSlot.RFinger, ref entry->Items[11]); + Add(EquipSlot.LFinger, ref entry->Items[12]); } _movedItemsEvent.Invoke(_itemList.ToArray()); @@ -118,6 +129,13 @@ public sealed unsafe class InventoryService : IDisposable, IRequiredService return ret; } + private int EquipGearSetDetour(RaptureGearsetModule* module, int gearsetId, byte glamourPlateId) + { + var ret = _equipGearsetHook.Original(module, gearsetId, glamourPlateId); + Glamourer.Log.Verbose($"[InventoryService] Applied gear set {gearsetId} with glamour plate {glamourPlateId} (Returned {ret})"); + return ret; + } + private static uint FixId(uint itemId) => itemId % 50000; @@ -130,7 +148,7 @@ public sealed unsafe class InventoryService : IDisposable, IRequiredService InventoryType targetContainer, ushort targetSlot, byte unk) { var ret = _moveItemHook.Original(manager, sourceContainer, sourceSlot, targetContainer, targetSlot, unk); - Glamourer.Log.Excessive($"[InventoryService] Moved {sourceContainer} {sourceSlot} {targetContainer} {targetSlot} (Returned {ret})"); + Glamourer.Log.Verbose($"[InventoryService] Moved {sourceContainer} {sourceSlot} {targetContainer} {targetSlot} (Returned {ret})"); if (ret == 0) { if (InvokeSource(sourceContainer, sourceSlot, out var source)) diff --git a/Glamourer/Interop/JobService.cs b/Glamourer/Interop/JobService.cs index 1797809..f687715 100644 --- a/Glamourer/Interop/JobService.cs +++ b/Glamourer/Interop/JobService.cs @@ -50,7 +50,7 @@ public class JobService : IDisposable var newJob = Jobs.TryGetValue(newJobIndex, out var j) ? j : Jobs[0]; var oldJob = Jobs.TryGetValue(oldJobIndex, out var o) ? o : Jobs[0]; - Glamourer.Log.Excessive($"{actor} changed job from {oldJob} to {newJob}."); + Glamourer.Log.Error($"{actor} changed job from {oldJob} to {newJob}."); JobChanged?.Invoke(actor, oldJob, newJob); } } diff --git a/Glamourer/Interop/UpdateSlotService.cs b/Glamourer/Interop/UpdateSlotService.cs index d1004e6..e43dc74 100644 --- a/Glamourer/Interop/UpdateSlotService.cs +++ b/Glamourer/Interop/UpdateSlotService.cs @@ -1,36 +1,124 @@ using Dalamud.Hooking; using Dalamud.Plugin.Services; using Dalamud.Utility.Signatures; +using FFXIVClientStructs.FFXIV.Client.Game.Character; using Glamourer.Events; using Penumbra.GameData; using Penumbra.GameData.DataContainers; using Penumbra.GameData.Enums; using Penumbra.GameData.Interop; using Penumbra.GameData.Structs; - namespace Glamourer.Interop; +/// +/// This struct is the struct that loadallequipment passes in as its gearsetData container. +/// +[StructLayout(LayoutKind.Explicit)] // Size of 70 bytes maybe? +public readonly struct GearsetItemDataStruct +{ + // Stores the weapon data. Includes both dyes in the data. + [FieldOffset(0)] public readonly WeaponModelId MainhandWeaponData; + [FieldOffset(8)] public readonly WeaponModelId OffhandWeaponData; + + [FieldOffset(16)] public readonly byte CrestBitField; // A Bitfield:: ShieldCrest == 1, HeadCrest == 2, Chest Crest == 4 + [FieldOffset(17)] public readonly byte JobId; // Job ID associated with the gearset change. + + // Flicks from 0 to 128 (anywhere inbetween), have yet to associate what it is linked to. Remains the same when flicking between gearsets of the same job. + [FieldOffset(18)] public readonly byte UNK_18; + [FieldOffset(19)] public readonly byte UNK_19; // I have never seen this be anything other than 0. + + // Legacy helmet equip slot armor for a character. + [FieldOffset(20)] public readonly LegacyCharacterArmor HeadSlotArmor; + [FieldOffset(24)] public readonly LegacyCharacterArmor TopSlotArmor; + [FieldOffset(28)] public readonly LegacyCharacterArmor ArmsSlotArmor; + [FieldOffset(32)] public readonly LegacyCharacterArmor LegsSlotArmor; + [FieldOffset(26)] public readonly LegacyCharacterArmor FeetSlotArmor; + [FieldOffset(40)] public readonly LegacyCharacterArmor EarSlotArmor; + [FieldOffset(44)] public readonly LegacyCharacterArmor NeckSlotArmor; + [FieldOffset(48)] public readonly LegacyCharacterArmor WristSlotArmor; + [FieldOffset(52)] public readonly LegacyCharacterArmor RFingerSlotArmor; + [FieldOffset(56)] public readonly LegacyCharacterArmor LFingerSlotArmor; + + // Byte array of all slot's secondary dyes. + [FieldOffset(60)] public readonly byte HeadSlotSecondaryDye; + [FieldOffset(61)] public readonly byte TopSlotSecondaryDye; + [FieldOffset(62)] public readonly byte ArmsSlotSecondaryDye; + [FieldOffset(63)] public readonly byte LegsSlotSecondaryDye; + [FieldOffset(64)] public readonly byte FeetSlotSecondaryDye; + [FieldOffset(65)] public readonly byte EarSlotSecondaryDye; + [FieldOffset(66)] public readonly byte NeckSlotSecondaryDye; + [FieldOffset(67)] public readonly byte WristSlotSecondaryDye; + [FieldOffset(68)] public readonly byte RFingerSlotSecondaryDye; + [FieldOffset(69)] public readonly byte LFingerSlotSecondaryDye; +} + public unsafe class UpdateSlotService : IDisposable { - public readonly EquipSlotUpdating EquipSlotUpdatingEvent; - public readonly BonusSlotUpdating BonusSlotUpdatingEvent; - private readonly DictBonusItems _bonusItems; + public readonly EquipSlotUpdating EquipSlotUpdatingEvent; + public readonly BonusSlotUpdating BonusSlotUpdatingEvent; + private readonly DictBonusItems _bonusItems; + + #region LoadAllEquipData + /////////////////////////////////////////////////// + // This is a currently undocumented signature that loads all equipment after changing a gearset. + // :: Signature Maintainers Note: + // To obtain this signature, get the stacktrace from FlagSlotForUpdate for human, and find func `sub_140842F50`. + // This function is what calls the weapon/equipment/crest loads, which call FlagSlotForUpdate if different. + // + // By detouring this function, and executing the original, then logic after, we have a consistant point in time where we know all + // slots have been flagged, meaning a consistant point in time that glamourer has processed all of its updates. + public const string LoadAllEquipmentSig = "48 89 5C 24 ?? 55 56 57 41 54 41 55 41 56 41 57 48 81 EC ?? ?? ?? ?? 48 8B 05 ?? ?? ?? ?? 48 33 C4 48 89 84 24 ?? ?? ?? ?? 44 0F B6 B9"; + private delegate Int64 LoadAllEquipmentDelegate(DrawDataContainer* drawDataContainer, GearsetItemDataStruct* gearsetData); + private Int64 LoadAllEquipmentDetour(DrawDataContainer* drawDataContainer, GearsetItemDataStruct* gearsetData) + { + // return original first so we can log the changes after + var ret = _loadAllEquipmentHook.Original(drawDataContainer, gearsetData); + + // perform logic stuff. + var owner = drawDataContainer->OwnerObject; + Glamourer.Log.Warning($"[LoadAllEquipmentDetour] Owner: 0x{(nint)owner->DrawObject:X} Finished Applying its GameState!"); + Glamourer.Log.Warning($"[LoadAllEquipmentDetour] GearsetItemData: {FormatGearsetItemDataStruct(*gearsetData)}"); + + // return original. + return ret; + } + + private string FormatWeaponModelId(WeaponModelId weaponModelId) => $"Id: {weaponModelId.Id}, Type: {weaponModelId.Type}, Variant: {weaponModelId.Variant}, Stain0: {weaponModelId.Stain0}, Stain1: {weaponModelId.Stain1}"; + + private string FormatGearsetItemDataStruct(GearsetItemDataStruct gearsetItemData) + { + string ret = $"\nMainhandWeaponData: {FormatWeaponModelId(gearsetItemData.MainhandWeaponData)}," + + $"\nOffhandWeaponData: {FormatWeaponModelId(gearsetItemData.OffhandWeaponData)}," + + $"\nCrestBitField: {gearsetItemData.CrestBitField} | JobId: {gearsetItemData.JobId} | UNK_18: {gearsetItemData.UNK_18} | UNK_19: {gearsetItemData.UNK_19}"; + // Iterate through offsets from 20 to 60 and format the CharacterArmor data + for (int offset = 20; offset <= 56; offset += sizeof(LegacyCharacterArmor)) + { + LegacyCharacterArmor* equipSlotPtr = (LegacyCharacterArmor*)((byte*)&gearsetItemData + offset); + int dyeOffset = (offset - 20) / sizeof(LegacyCharacterArmor) + 60; // Calculate the corresponding dye offset + byte* dyePtr = (byte*)&gearsetItemData + dyeOffset; + ret += $"\nEquipSlot {((EquipSlot)(dyeOffset-60)).ToString()}:: Id: {(*equipSlotPtr).Set}, Variant: {(*equipSlotPtr).Variant}, Stain0: {(*equipSlotPtr).Stain.Id}, Stain1: {*dyePtr}"; + } + return ret; + } +#endregion LoadAllEquipData public UpdateSlotService(EquipSlotUpdating equipSlotUpdating, BonusSlotUpdating bonusSlotUpdating, IGameInteropProvider interop, DictBonusItems bonusItems) { EquipSlotUpdatingEvent = equipSlotUpdating; BonusSlotUpdatingEvent = bonusSlotUpdating; - _bonusItems = bonusItems; + _bonusItems = bonusItems; interop.InitializeFromAttributes(this); _flagSlotForUpdateHook.Enable(); _flagBonusSlotForUpdateHook.Enable(); + _loadAllEquipmentHook.Enable(); } public void Dispose() { _flagSlotForUpdateHook.Dispose(); _flagBonusSlotForUpdateHook.Dispose(); + _loadAllEquipmentHook.Dispose(); } public void UpdateEquipSlot(Model drawObject, EquipSlot slot, CharacterArmor data) @@ -79,24 +167,36 @@ public unsafe class UpdateSlotService : IDisposable [Signature(Sigs.FlagBonusSlotForUpdate, DetourName = nameof(FlagBonusSlotForUpdateDetour))] private readonly Hook _flagBonusSlotForUpdateHook = null!; + [Signature(LoadAllEquipmentSig, DetourName = nameof(LoadAllEquipmentDetour))] + private readonly Hook _loadAllEquipmentHook = null!; + private ulong FlagSlotForUpdateDetour(nint drawObject, uint slotIdx, CharacterArmor* data) { - var slot = slotIdx.ToEquipSlot(); + var slot = slotIdx.ToEquipSlot(); var returnValue = ulong.MaxValue; + EquipSlotUpdatingEvent.Invoke(drawObject, slot, ref *data, ref returnValue); - Glamourer.Log.Excessive($"[FlagSlotForUpdate] Called with 0x{drawObject:X} for slot {slot} with {*data} ({returnValue:X})."); - return returnValue == ulong.MaxValue ? _flagSlotForUpdateHook.Original(drawObject, slotIdx, data) : returnValue; + Glamourer.Log.Information($"[FlagSlotForUpdate] Called with 0x{drawObject:X} for slot {slot} with {*data} ({returnValue:X})."); + returnValue = returnValue == ulong.MaxValue ? _flagSlotForUpdateHook.Original(drawObject, slotIdx, data) : returnValue; + + return returnValue; } private ulong FlagBonusSlotForUpdateDetour(nint drawObject, uint slotIdx, CharacterArmor* data) { - var slot = slotIdx.ToBonusSlot(); + var slot = slotIdx.ToBonusSlot(); var returnValue = ulong.MaxValue; + BonusSlotUpdatingEvent.Invoke(drawObject, slot, ref *data, ref returnValue); - Glamourer.Log.Excessive($"[FlagBonusSlotForUpdate] Called with 0x{drawObject:X} for slot {slot} with {*data} ({returnValue:X})."); - return returnValue == ulong.MaxValue ? _flagBonusSlotForUpdateHook.Original(drawObject, slotIdx, data) : returnValue; + Glamourer.Log.Information($"[FlagBonusSlotForUpdate] Called with 0x{drawObject:X} for slot {slot} with {*data} ({returnValue:X})."); + returnValue = returnValue == ulong.MaxValue ? _flagBonusSlotForUpdateHook.Original(drawObject, slotIdx, data) : returnValue; + + return returnValue; } private ulong FlagSlotForUpdateInterop(Model drawObject, EquipSlot slot, CharacterArmor armor) - => _flagSlotForUpdateHook.Original(drawObject.Address, slot.ToIndex(), &armor); + { + Glamourer.Log.Warning($"Glamour-Invoked Equip Slot update for 0x{drawObject.Address:X} with {slot} and {armor}."); + return _flagSlotForUpdateHook.Original(drawObject.Address, slot.ToIndex(), &armor); + } } diff --git a/Glamourer/State/StateEditor.cs b/Glamourer/State/StateEditor.cs index f9ddb89..b592b65 100644 --- a/Glamourer/State/StateEditor.cs +++ b/Glamourer/State/StateEditor.cs @@ -51,7 +51,7 @@ public class StateEditor( return; var actors = Applier.ChangeCustomize(state, settings.Source.RequiresChange()); - Glamourer.Log.Verbose( + Glamourer.Log.Information( $"Set {idx.ToDefaultName()} customizations in state {state.Identifier.Incognito(null)} from {old.Value} to {value.Value}. [Affecting {actors.ToLazyString("nothing")}.]"); StateChanged.Invoke(StateChangeType.Customize, settings.Source, state, actors, new CustomizeTransaction(idx, old, value)); } @@ -64,7 +64,7 @@ public class StateEditor( return; var actors = Applier.ChangeCustomize(state, settings.Source.RequiresChange()); - Glamourer.Log.Verbose( + Glamourer.Log.Information( $"Set {applied} customizations in state {state.Identifier.Incognito(null)} from {old} to {customizeInput}. [Affecting {actors.ToLazyString("nothing")}.]"); StateChanged.Invoke(StateChangeType.EntireCustomize, settings.Source, state, actors, new EntireCustomizeTransaction(applied, old, customizeInput)); @@ -75,7 +75,10 @@ public class StateEditor( { var state = (ActorState)data; if (!Editor.ChangeItem(state, slot, item, settings.Source, out var old, settings.Key)) + { + Glamourer.Log.Information("Not Setting State or invoking, Editor requested us not to change it!"); return; + } var type = slot.ToIndex() < 10 ? StateChangeType.Equip : StateChangeType.Weapon; var actors = type is StateChangeType.Equip @@ -86,8 +89,8 @@ public class StateEditor( if (slot is EquipSlot.MainHand) ApplyMainhandPeriphery(state, item, null, settings); - Glamourer.Log.Verbose( - $"Set {slot.ToName()} in state {state.Identifier.Incognito(null)} from {old.Name} ({old.ItemId}) to {item.Name} ({item.ItemId}). [Affecting {actors.ToLazyString("nothing")}.]"); + Glamourer.Log.Debug( + $"[ChangeItem] Set {slot.ToName()} in state {state.Identifier.Incognito(null)} from {old.Name} ({old.ItemId}) to {item.Name} ({item.ItemId}). [Affecting {actors.ToLazyString("nothing")}.]"); if (type is StateChangeType.Equip) { @@ -116,8 +119,8 @@ public class StateEditor( return; var actors = Applier.ChangeBonusItem(state, slot, settings.Source.RequiresChange()); - Glamourer.Log.Verbose( - $"Set {slot.ToName()} in state {state.Identifier.Incognito(null)} from {old.Name} ({old.Id}) to {item.Name} ({item.Id}). [Affecting {actors.ToLazyString("nothing")}.]"); + Glamourer.Log.Debug( + $"[ChangeBonus] Set {slot.ToName()} in state {state.Identifier.Incognito(null)} from {old.Name} ({old.Id}) to {item.Name} ({item.Id}). [Affecting {actors.ToLazyString("nothing")}.]"); StateChanged.Invoke(StateChangeType.BonusItem, settings.Source, state, actors, new BonusItemTransaction(slot, old, item)); } @@ -149,8 +152,8 @@ public class StateEditor( if (slot is EquipSlot.MainHand) ApplyMainhandPeriphery(state, item, stains, settings); - Glamourer.Log.Verbose( - $"Set {slot.ToName()} in state {state.Identifier.Incognito(null)} from {old.Name} ({old.ItemId}) to {item!.Value.Name} ({item.Value.ItemId}) and its stain from {oldStains} to {stains!.Value}. [Affecting {actors.ToLazyString("nothing")}.]"); + Glamourer.Log.Debug( + $"[ChangeEquip] Set {slot.ToName()} in state {state.Identifier.Incognito(null)} from {old.Name} ({old.ItemId}) to {item!.Value.Name} ({item.Value.ItemId}) and its stain from {oldStains} to {stains!.Value}. [Affecting {actors.ToLazyString("nothing")}.]"); if (type is StateChangeType.Equip) { StateChanged.Invoke(type, settings.Source, state, actors, new EquipTransaction(slot, old, item!.Value)); @@ -181,7 +184,7 @@ public class StateEditor( return; var actors = Applier.ChangeStain(state, slot, settings.Source.RequiresChange()); - Glamourer.Log.Verbose( + Glamourer.Log.Debug( $"Set {slot.ToName()} stain in state {state.Identifier.Incognito(null)} from {old} to {stains}. [Affecting {actors.ToLazyString("nothing")}.]"); StateChanged.Invoke(StateChangeType.Stains, settings.Source, state, actors, new StainTransaction(slot, old, stains)); } @@ -250,7 +253,7 @@ public class StateEditor( return; var actors = Applier.ChangeMetaState(state, index, settings.Source.RequiresChange()); - Glamourer.Log.Verbose( + Glamourer.Log.Debug( $"Set {index.ToName()} in state {state.Identifier.Incognito(null)} from {old} to {value}. [Affecting {actors.ToLazyString("nothing")}.]"); StateChanged.Invoke(StateChangeType.Other, settings.Source, state, actors, new MetaTransaction(index, old, value)); } @@ -414,8 +417,7 @@ public class StateEditor( ? Applier.ApplyAll(state, requiresRedraw, false) : ActorData.Invalid; - Glamourer.Log.Verbose( - $"Applied design to {state.Identifier.Incognito(null)}. [Affecting {actors.ToLazyString("nothing")}.]"); + Glamourer.Log.Debug($"Applied design to {state.Identifier.Incognito(null)}. [Affecting {actors.ToLazyString("nothing")}.]"); StateChanged.Invoke(StateChangeType.Design, state.Sources[MetaIndex.Wetness], state, actors, null); // FIXME: maybe later return; diff --git a/Glamourer/State/StateListener.cs b/Glamourer/State/StateListener.cs index d054a25..382e488 100644 --- a/Glamourer/State/StateListener.cs +++ b/Glamourer/State/StateListener.cs @@ -216,8 +216,7 @@ public class StateListener : IDisposable // then we do not want to use our restricted gear protection // since we assume the player has that gear modded to availability. var locked = false; - if (actor.Identifier(_actors, out var identifier) - && _manager.TryGetValue(identifier, out var state)) + if (actor.Identifier(_actors, out var identifier) && _manager.TryGetValue(identifier, out var state)) { HandleEquipSlot(actor, state, slot, ref armor); locked = state.Sources[slot, false] is StateSource.IpcFixed; @@ -383,7 +382,7 @@ public class StateListener : IDisposable lastFistOffhand = new CharacterWeapon((PrimaryId)(weapon.Skeleton.Id + 50), weapon.Weapon, weapon.Variant, weapon.Stains); _fistOffhands[actor] = lastFistOffhand; - Glamourer.Log.Excessive($"Storing fist weapon offhand {lastFistOffhand} for 0x{actor.Address:X}."); + Glamourer.Log.Verbose($"Storing fist weapon offhand {lastFistOffhand} for 0x{actor.Address:X}."); } _funModule.ApplyFunToWeapon(actor, ref weapon, slot); diff --git a/Glamourer/State/StateManager.cs b/Glamourer/State/StateManager.cs index eabaf2f..736dd6e 100644 --- a/Glamourer/State/StateManager.cs +++ b/Glamourer/State/StateManager.cs @@ -273,7 +273,7 @@ public sealed class StateManager( if (source is not StateSource.Game) actors = Applier.ApplyAll(state, redraw, true); - Glamourer.Log.Verbose( + Glamourer.Log.Debug( $"Reset entire state of {state.Identifier.Incognito(null)} to game base. [Affecting {actors.ToLazyString("nothing")}.]"); StateChanged.Invoke(StateChangeType.Reset, source, state, actors, null); } @@ -298,7 +298,7 @@ public sealed class StateManager( state.Materials.Clear(); - Glamourer.Log.Verbose( + Glamourer.Log.Debug( $"Reset advanced customization and dye state of {state.Identifier.Incognito(null)} to game base. [Affecting {actors.ToLazyString("nothing")}.]"); StateChanged.Invoke(StateChangeType.Reset, source, state, actors, null); } From e1a41b5f3c7d144cae20c291ff2ee2fe77978e87 Mon Sep 17 00:00:00 2001 From: Cordelia Mist Date: Fri, 17 Jan 2025 17:39:26 -0800 Subject: [PATCH 02/10] Implements true endpoints for all glamourer operations, also correctly marks reverts and gearsets. Replaced back excessive logging to maintain with logging formats expected by glamourer. --- Glamourer/Api/DesignsApi.cs | 2 +- Glamourer/Api/IpcProviders.cs | 1 + Glamourer/Api/StateApi.cs | 27 ++++- Glamourer/Automation/AutoDesignApplier.cs | 2 - Glamourer/Designs/IDesignEditor.cs | 6 +- Glamourer/Events/GearsetDataLoaded.cs | 23 ++++ Glamourer/Events/StateUpdated.cs | 26 +++++ Glamourer/Gui/DesignQuickBar.cs | 8 +- Glamourer/Gui/Tabs/ActorTab/ActorPanel.cs | 14 +-- .../Gui/Tabs/DebugTab/GlamourPlatePanel.cs | 2 +- Glamourer/Gui/Tabs/DesignTab/DesignPanel.cs | 4 +- Glamourer/Gui/Tabs/NpcTab/NpcPanel.cs | 4 +- Glamourer/Interop/ChangeCustomizeService.cs | 4 +- Glamourer/Interop/InventoryService.cs | 10 +- Glamourer/Interop/JobService.cs | 2 +- .../Interop/Penumbra/PenumbraAutoRedraw.cs | 4 +- Glamourer/Interop/UpdateSlotService.cs | 100 ++++++++---------- Glamourer/Services/CommandService.cs | 8 +- Glamourer/State/StateEditor.cs | 5 + Glamourer/State/StateListener.cs | 29 ++++- Glamourer/State/StateManager.cs | 43 +++++++- 21 files changed, 225 insertions(+), 99 deletions(-) create mode 100644 Glamourer/Events/GearsetDataLoaded.cs create mode 100644 Glamourer/Events/StateUpdated.cs diff --git a/Glamourer/Api/DesignsApi.cs b/Glamourer/Api/DesignsApi.cs index ee49bd5..6c3037e 100644 --- a/Glamourer/Api/DesignsApi.cs +++ b/Glamourer/Api/DesignsApi.cs @@ -33,7 +33,7 @@ public class DesignsApi(ApiHelpers helpers, DesignManager designs, StateManager { var once = (flags & ApplyFlag.Once) != 0; var settings = new ApplySettings(Source: once ? StateSource.IpcManual : StateSource.IpcFixed, Key: key, MergeLinks: true, - ResetMaterials: !once && key != 0); + ResetMaterials: !once && key != 0, SendStateUpdate: true); using var restrict = ApiHelpers.Restrict(design, flags); stateManager.ApplyDesign(state, design, settings); diff --git a/Glamourer/Api/IpcProviders.cs b/Glamourer/Api/IpcProviders.cs index 8639a22..166245f 100644 --- a/Glamourer/Api/IpcProviders.cs +++ b/Glamourer/Api/IpcProviders.cs @@ -52,6 +52,7 @@ public sealed class IpcProviders : IDisposable, IApiService IpcSubscribers.RevertToAutomationName.Provider(pi, api.State), IpcSubscribers.StateChanged.Provider(pi, api.State), IpcSubscribers.StateChangedWithType.Provider(pi, api.State), + IpcSubscribers.StateUpdated.Provider(pi, api.State), IpcSubscribers.GPoseChanged.Provider(pi, api.State), ]; _initializedProvider.Invoke(); diff --git a/Glamourer/Api/StateApi.cs b/Glamourer/Api/StateApi.cs index ce7d226..248e294 100644 --- a/Glamourer/Api/StateApi.cs +++ b/Glamourer/Api/StateApi.cs @@ -11,9 +11,10 @@ using OtterGui.Services; using Penumbra.GameData.Interop; using ObjectManager = Glamourer.Interop.ObjectManager; using StateChanged = Glamourer.Events.StateChanged; +using StateUpdated = Glamourer.Events.StateUpdated; namespace Glamourer.Api; - + public sealed class StateApi : IGlamourerApiState, IApiService, IDisposable { private readonly ApiHelpers _helpers; @@ -23,6 +24,7 @@ public sealed class StateApi : IGlamourerApiState, IApiService, IDisposable private readonly AutoDesignApplier _autoDesigns; private readonly ObjectManager _objects; private readonly StateChanged _stateChanged; + private readonly StateUpdated _stateUpdated; private readonly GPoseService _gPose; public StateApi(ApiHelpers helpers, @@ -32,6 +34,7 @@ public sealed class StateApi : IGlamourerApiState, IApiService, IDisposable AutoDesignApplier autoDesigns, ObjectManager objects, StateChanged stateChanged, + StateUpdated stateUpdated, GPoseService gPose) { _helpers = helpers; @@ -41,8 +44,10 @@ public sealed class StateApi : IGlamourerApiState, IApiService, IDisposable _autoDesigns = autoDesigns; _objects = objects; _stateChanged = stateChanged; + _stateUpdated = stateUpdated; _gPose = gPose; _stateChanged.Subscribe(OnStateChanged, Events.StateChanged.Priority.GlamourerIpc); + _stateUpdated.Subscribe(OnStateUpdated, Events.StateUpdated.Priority.GlamourerIpc); _gPose.Subscribe(OnGPoseChange, GPoseService.Priority.GlamourerIpc); } @@ -250,13 +255,14 @@ public sealed class StateApi : IGlamourerApiState, IApiService, IDisposable public event Action? StateChanged; public event Action? StateChangedWithType; + public event Action? StateUpdated; public event Action? GPoseChanged; private void ApplyDesign(ActorState state, DesignBase design, uint key, ApplyFlag flags) { var once = (flags & ApplyFlag.Once) != 0; var settings = new ApplySettings(Source: once ? StateSource.IpcManual : StateSource.IpcFixed, Key: key, MergeLinks: true, - ResetMaterials: !once && key != 0); + ResetMaterials: !once && key != 0, SendStateUpdate: true); _stateManager.ApplyDesign(state, design, settings); ApiHelpers.Lock(state, key, flags); } @@ -296,7 +302,7 @@ public sealed class StateApi : IGlamourerApiState, IApiService, IDisposable { var source = (flags & ApplyFlag.Once) != 0 ? StateSource.IpcManual : StateSource.IpcFixed; _autoDesigns.ReapplyAutomation(actor, state.Identifier, state, true, out var forcedRedraw); - _stateManager.ReapplyState(actor, state, forcedRedraw, source); + _stateManager.ReapplyAutomationState(actor, state, forcedRedraw, true, source); ApiHelpers.Lock(state, key, flags); } @@ -333,7 +339,8 @@ public sealed class StateApi : IGlamourerApiState, IApiService, IDisposable private void OnStateChanged(StateChangeType type, StateSource _2, ActorState _3, ActorData actors, ITransaction? _5) { - Glamourer.Log.Error($"[OnStateChanged API CALL] Sending out OnStateChanged with type {type}."); + // Remove this comment before creating PR. + Glamourer.Log.Verbose($"[OnStateChanged] Sending out OnStateChanged with type {type}."); if (StateChanged != null) foreach (var actor in actors.Objects) @@ -343,4 +350,16 @@ public sealed class StateApi : IGlamourerApiState, IApiService, IDisposable foreach (var actor in actors.Objects) StateChangedWithType.Invoke(actor.Address, type); } + + private void OnStateUpdated(StateUpdateType type, ActorData actors) + { + if (StateUpdated != null) + foreach (var actor in actors.Objects) + { + // Remove these before creating PR + Glamourer.Log.Information($"[ENDPOINT DEBUGGING] 0x{actor.Address:X} had update of type {type}."); + Glamourer.Log.Information("--------------------------------------------------------------"); + StateUpdated.Invoke(actor.Address, type); + } + } } diff --git a/Glamourer/Automation/AutoDesignApplier.cs b/Glamourer/Automation/AutoDesignApplier.cs index d49864f..bcc907c 100644 --- a/Glamourer/Automation/AutoDesignApplier.cs +++ b/Glamourer/Automation/AutoDesignApplier.cs @@ -216,8 +216,6 @@ public sealed class AutoDesignApplier : IDisposable if (!_config.EnableAutoDesigns || !actor.Identifier(_actors, out var id)) return; - Glamourer.Log.Information($"[AutoDesignApplier][OnJobChange] We had EnableAutoDesigns active, and are a valid actor!"); - if (!GetPlayerSet(id, out var set)) { if (_state.TryGetValue(id, out var s)) diff --git a/Glamourer/Designs/IDesignEditor.cs b/Glamourer/Designs/IDesignEditor.cs index 935263b..0178620 100644 --- a/Glamourer/Designs/IDesignEditor.cs +++ b/Glamourer/Designs/IDesignEditor.cs @@ -13,7 +13,8 @@ public readonly record struct ApplySettings( bool FromJobChange = false, bool UseSingleSource = false, bool MergeLinks = false, - bool ResetMaterials = false) + bool ResetMaterials = false, + bool SendStateUpdate = false) { public static readonly ApplySettings Manual = new() { @@ -24,6 +25,7 @@ public readonly record struct ApplySettings( UseSingleSource = false, MergeLinks = false, ResetMaterials = false, + SendStateUpdate = false, }; public static readonly ApplySettings ManualWithLinks = new() @@ -35,6 +37,7 @@ public readonly record struct ApplySettings( UseSingleSource = false, MergeLinks = true, ResetMaterials = false, + SendStateUpdate = false, }; public static readonly ApplySettings Game = new() @@ -46,6 +49,7 @@ public readonly record struct ApplySettings( UseSingleSource = false, MergeLinks = false, ResetMaterials = true, + SendStateUpdate = false, }; } diff --git a/Glamourer/Events/GearsetDataLoaded.cs b/Glamourer/Events/GearsetDataLoaded.cs new file mode 100644 index 0000000..47d0108 --- /dev/null +++ b/Glamourer/Events/GearsetDataLoaded.cs @@ -0,0 +1,23 @@ +using OtterGui.Classes; +using Penumbra.GameData.Enums; +using Penumbra.GameData.Interop; +using Penumbra.GameData.Structs; + +namespace Glamourer.Events; + +/// +/// Triggers when the equipped gearset finished running all of its LoadEquipment, LoadWeapon, and crest calls. +/// This defines a universal endpoint of base game state application to monitor. +/// +/// The model drawobject associated with the finished load (should always be ClientPlayer) +/// +/// +public sealed class GearsetDataLoaded() + : EventWrapper(nameof(GearsetDataLoaded)) +{ + public enum Priority + { + /// + StateListener = 0, + } +} \ No newline at end of file diff --git a/Glamourer/Events/StateUpdated.cs b/Glamourer/Events/StateUpdated.cs new file mode 100644 index 0000000..82d737f --- /dev/null +++ b/Glamourer/Events/StateUpdated.cs @@ -0,0 +1,26 @@ +using Glamourer.Api.Enums; +using Glamourer.Designs.History; +using Glamourer.Interop.Structs; +using Glamourer.State; +using OtterGui.Classes; + +namespace Glamourer.Events; + +/// +/// Triggered when a Design is edited in any way. +/// +/// Parameter is the type of the change +/// Parameter is the changed saved state. +/// Parameter is the existing actors using this saved state. +/// Parameter is any additional data depending on the type of change. +/// +/// +public sealed class StateUpdated() + : EventWrapper(nameof(StateUpdated)) +{ + public enum Priority + { + /// + GlamourerIpc = int.MinValue, + } +} diff --git a/Glamourer/Gui/DesignQuickBar.cs b/Glamourer/Gui/DesignQuickBar.cs index 31ca45e..bdc345f 100644 --- a/Glamourer/Gui/DesignQuickBar.cs +++ b/Glamourer/Gui/DesignQuickBar.cs @@ -183,7 +183,7 @@ public sealed class DesignQuickBar : Window, IDisposable } using var _ = design!.TemporarilyRestrictApplication(ApplicationCollection.FromKeys()); - _stateManager.ApplyDesign(state, design, ApplySettings.ManualWithLinks); + _stateManager.ApplyDesign(state, design, ApplySettings.ManualWithLinks with { SendStateUpdate = true }); } private void DrawRevertButton(Vector2 buttonSize) @@ -213,7 +213,7 @@ public sealed class DesignQuickBar : Window, IDisposable var (clicked, _, _, state) = ResolveTarget(FontAwesomeIcon.UndoAlt, buttonSize, tooltip, available); ImGui.SameLine(); if (clicked) - _stateManager.ResetState(state!, StateSource.Manual); + _stateManager.ResetState(state!, StateSource.Manual, stateUpdate: true); } private void DrawRevertAutomationButton(Vector2 buttonSize) @@ -252,7 +252,7 @@ public sealed class DesignQuickBar : Window, IDisposable foreach (var actor in data.Objects) { _autoDesignApplier.ReapplyAutomation(actor, id, state!, true, out var forcedRedraw); - _stateManager.ReapplyState(actor, forcedRedraw, StateSource.Manual); + _stateManager.ReapplyAutomationState(actor, forcedRedraw, true, StateSource.Manual); } } @@ -292,7 +292,7 @@ public sealed class DesignQuickBar : Window, IDisposable foreach (var actor in data.Objects) { _autoDesignApplier.ReapplyAutomation(actor, id, state!, false, out var forcedRedraw); - _stateManager.ReapplyState(actor, forcedRedraw, StateSource.Manual); + _stateManager.ReapplyAutomationState(actor, forcedRedraw, false, StateSource.Manual); } } diff --git a/Glamourer/Gui/Tabs/ActorTab/ActorPanel.cs b/Glamourer/Gui/Tabs/ActorTab/ActorPanel.cs index 265e1d9..ced78fb 100644 --- a/Glamourer/Gui/Tabs/ActorTab/ActorPanel.cs +++ b/Glamourer/Gui/Tabs/ActorTab/ActorPanel.cs @@ -385,7 +385,7 @@ public class ActorPanel { if (ImGuiUtil.DrawDisabledButton("Revert to Game", Vector2.Zero, "Revert the character to its actual state in the game.", _state!.IsLocked)) - _stateManager.ResetState(_state!, StateSource.Manual); + _stateManager.ResetState(_state!, StateSource.Manual, stateUpdate: true); ImGui.SameLine(); @@ -394,7 +394,7 @@ public class ActorPanel !_config.EnableAutoDesigns || _state!.IsLocked)) { _autoDesignApplier.ReapplyAutomation(_actor, _identifier, _state!, false, out var forcedRedraw); - _stateManager.ReapplyState(_actor, forcedRedraw, StateSource.Manual); + _stateManager.ReapplyAutomationState(_actor, forcedRedraw, false, StateSource.Manual); } ImGui.SameLine(); @@ -403,14 +403,14 @@ public class ActorPanel !_config.EnableAutoDesigns || _state!.IsLocked)) { _autoDesignApplier.ReapplyAutomation(_actor, _identifier, _state!, true, out var forcedRedraw); - _stateManager.ReapplyState(_actor, forcedRedraw, StateSource.Manual); + _stateManager.ReapplyAutomationState(_actor, forcedRedraw, true, StateSource.Manual); } ImGui.SameLine(); if (ImGuiUtil.DrawDisabledButton("Reapply", Vector2.Zero, "Try to reapply the configured state if something went wrong. Should generally not be necessary.", _state!.IsLocked)) - _stateManager.ReapplyState(_actor, false, StateSource.Manual); + _stateManager.ReapplyState(_actor, false, StateSource.Manual, true); } private void DrawApplyToSelf() @@ -423,7 +423,7 @@ public class ActorPanel if (_stateManager.GetOrCreate(id, data.Objects[0], out var state)) _stateManager.ApplyDesign(state, _converter.Convert(_state!, ApplicationRules.FromModifiers(_state!)), - ApplySettings.Manual); + ApplySettings.Manual with { SendStateUpdate = true }); } private void DrawApplyToTarget() @@ -440,7 +440,7 @@ public class ActorPanel if (_stateManager.GetOrCreate(id, data.Objects[0], out var state)) _stateManager.ApplyDesign(state, _converter.Convert(_state!, ApplicationRules.FromModifiers(_state!)), - ApplySettings.Manual); + ApplySettings.Manual with { SendStateUpdate = true }); } @@ -467,7 +467,7 @@ public class ActorPanel var text = ImGui.GetClipboardText(); var design = panel._converter.FromBase64(text, applyCustomize, applyGear, out _) ?? throw new Exception("The clipboard did not contain valid data."); - panel._stateManager.ApplyDesign(panel._state!, design, ApplySettings.ManualWithLinks); + panel._stateManager.ApplyDesign(panel._state!, design, ApplySettings.ManualWithLinks with { SendStateUpdate = true }); } catch (Exception ex) { diff --git a/Glamourer/Gui/Tabs/DebugTab/GlamourPlatePanel.cs b/Glamourer/Gui/Tabs/DebugTab/GlamourPlatePanel.cs index 394bd7f..c44a722 100644 --- a/Glamourer/Gui/Tabs/DebugTab/GlamourPlatePanel.cs +++ b/Glamourer/Gui/Tabs/DebugTab/GlamourPlatePanel.cs @@ -79,7 +79,7 @@ public unsafe class GlamourPlatePanel : IGameDataDrawer if (ImGuiUtil.DrawDisabledButton("Apply to Player", Vector2.Zero, string.Empty, !enabled)) { var design = CreateDesign(plate); - _state.ApplyDesign(state!, design, ApplySettings.Manual); + _state.ApplyDesign(state!, design, ApplySettings.Manual with { SendStateUpdate = true }); } using (ImRaii.Group()) diff --git a/Glamourer/Gui/Tabs/DesignTab/DesignPanel.cs b/Glamourer/Gui/Tabs/DesignTab/DesignPanel.cs index 070ca1e..fe71609 100644 --- a/Glamourer/Gui/Tabs/DesignTab/DesignPanel.cs +++ b/Glamourer/Gui/Tabs/DesignTab/DesignPanel.cs @@ -460,7 +460,7 @@ public class DesignPanel if (_state.GetOrCreate(id, data.Objects[0], out var state)) { using var _ = _selector.Selected!.TemporarilyRestrictApplication(ApplicationCollection.FromKeys()); - _state.ApplyDesign(state, _selector.Selected!, ApplySettings.ManualWithLinks); + _state.ApplyDesign(state, _selector.Selected!, ApplySettings.ManualWithLinks with { SendStateUpdate = true }); } } @@ -478,7 +478,7 @@ public class DesignPanel if (_state.GetOrCreate(id, data.Objects[0], out var state)) { using var _ = _selector.Selected!.TemporarilyRestrictApplication(ApplicationCollection.FromKeys()); - _state.ApplyDesign(state, _selector.Selected!, ApplySettings.ManualWithLinks); + _state.ApplyDesign(state, _selector.Selected!, ApplySettings.ManualWithLinks with { SendStateUpdate = true }); } } diff --git a/Glamourer/Gui/Tabs/NpcTab/NpcPanel.cs b/Glamourer/Gui/Tabs/NpcTab/NpcPanel.cs index 312bceb..345df11 100644 --- a/Glamourer/Gui/Tabs/NpcTab/NpcPanel.cs +++ b/Glamourer/Gui/Tabs/NpcTab/NpcPanel.cs @@ -196,7 +196,7 @@ public class NpcPanel if (_state.GetOrCreate(id, data.Objects[0], out var state)) { var design = _converter.Convert(ToDesignData(), new StateMaterialManager(), ApplicationRules.NpcFromModifiers()); - _state.ApplyDesign(state, design, ApplySettings.Manual); + _state.ApplyDesign(state, design, ApplySettings.Manual with { SendStateUpdate = true }); } } @@ -214,7 +214,7 @@ public class NpcPanel if (_state.GetOrCreate(id, data.Objects[0], out var state)) { var design = _converter.Convert(ToDesignData(), new StateMaterialManager(), ApplicationRules.NpcFromModifiers()); - _state.ApplyDesign(state, design, ApplySettings.Manual); + _state.ApplyDesign(state, design, ApplySettings.Manual with { SendStateUpdate = true }); } } diff --git a/Glamourer/Interop/ChangeCustomizeService.cs b/Glamourer/Interop/ChangeCustomizeService.cs index 032412e..10e3a12 100644 --- a/Glamourer/Interop/ChangeCustomizeService.cs +++ b/Glamourer/Interop/ChangeCustomizeService.cs @@ -70,7 +70,7 @@ public unsafe class ChangeCustomizeService : EventWrapperRef2 _itemList = new(12); - // This can be moved into client structs or penumbra.gamedata when needed. + // Called by EquipGearset, but returns a pointer instead of an int. + // This is the internal function processed by all sources of Equipping a gearset, + // such as hotbar gearset application and command gearset application public const string EquipGearsetInternal = "40 55 53 56 57 41 57 48 8D AC 24 ?? ?? ?? ?? 48 81 EC ?? ?? ?? ?? 48 8B 05 ?? ?? ?? ?? 48 33 C4 48 89 85 ?? ?? ?? ?? 4C 63 FA"; private delegate nint ChangeGearsetInternalDelegate(RaptureGearsetModule* module, uint gearsetId, byte glamourPlateId); @@ -56,7 +58,7 @@ public sealed unsafe class InventoryService : IDisposable, IRequiredService var ret = _equipGearsetInternalHook.Original(module, gearsetId, glamourPlateId); var set = module->GetGearset((int)gearsetId); _gearsetEvent.Invoke(new ByteString(set->Name).ToString(), (int)gearsetId, prior, glamourPlateId, set->ClassJob); - Glamourer.Log.Warning($"[InventoryService] [EquipInternal] Applied gear set {gearsetId} with glamour plate {glamourPlateId} (Returned {ret})"); + Glamourer.Log.Verbose($"[InventoryService] [EquipInternal] Applied gear set {gearsetId} with glamour plate {glamourPlateId} (Returned {ret})"); if (ret == 0) { var entry = module->GetGearset((int)gearsetId); @@ -132,7 +134,7 @@ public sealed unsafe class InventoryService : IDisposable, IRequiredService private int EquipGearSetDetour(RaptureGearsetModule* module, int gearsetId, byte glamourPlateId) { var ret = _equipGearsetHook.Original(module, gearsetId, glamourPlateId); - Glamourer.Log.Verbose($"[InventoryService] Applied gear set {gearsetId} with glamour plate {glamourPlateId} (Returned {ret})"); + Glamourer.Log.Excessive($"[InventoryService] (old) Applied gear set {gearsetId} with glamour plate {glamourPlateId} (Returned {ret})"); return ret; } @@ -148,7 +150,7 @@ public sealed unsafe class InventoryService : IDisposable, IRequiredService InventoryType targetContainer, ushort targetSlot, byte unk) { var ret = _moveItemHook.Original(manager, sourceContainer, sourceSlot, targetContainer, targetSlot, unk); - Glamourer.Log.Verbose($"[InventoryService] Moved {sourceContainer} {sourceSlot} {targetContainer} {targetSlot} (Returned {ret})"); + Glamourer.Log.Excessive($"[InventoryService] Moved {sourceContainer} {sourceSlot} {targetContainer} {targetSlot} (Returned {ret})"); if (ret == 0) { if (InvokeSource(sourceContainer, sourceSlot, out var source)) diff --git a/Glamourer/Interop/JobService.cs b/Glamourer/Interop/JobService.cs index f687715..1797809 100644 --- a/Glamourer/Interop/JobService.cs +++ b/Glamourer/Interop/JobService.cs @@ -50,7 +50,7 @@ public class JobService : IDisposable var newJob = Jobs.TryGetValue(newJobIndex, out var j) ? j : Jobs[0]; var oldJob = Jobs.TryGetValue(oldJobIndex, out var o) ? o : Jobs[0]; - Glamourer.Log.Error($"{actor} changed job from {oldJob} to {newJob}."); + Glamourer.Log.Excessive($"{actor} changed job from {oldJob} to {newJob}."); JobChanged?.Invoke(actor, oldJob, newJob); } } diff --git a/Glamourer/Interop/Penumbra/PenumbraAutoRedraw.cs b/Glamourer/Interop/Penumbra/PenumbraAutoRedraw.cs index fbe0d9d..3e48fe9 100644 --- a/Glamourer/Interop/Penumbra/PenumbraAutoRedraw.cs +++ b/Glamourer/Interop/Penumbra/PenumbraAutoRedraw.cs @@ -88,7 +88,7 @@ public class PenumbraAutoRedraw : IDisposable, IRequiredService _actions.Enqueue((state, () => { foreach (var actor in actors.Objects) - _state.ReapplyState(actor, state, false, StateSource.IpcManual); + _state.ReapplyState(actor, state, false, StateSource.IpcManual, true); Glamourer.Log.Debug($"Automatically applied mod settings of type {type} to {id.Incognito(null)}."); }, WaitFrames)); } @@ -108,7 +108,7 @@ public class PenumbraAutoRedraw : IDisposable, IRequiredService _frame = currentFrame; _framework.RunOnFrameworkThread(() => { - _state.ReapplyState(_objects.Player, false, StateSource.IpcManual); + _state.ReapplyState(_objects.Player, false, StateSource.IpcManual, true); Glamourer.Log.Debug( $"Automatically applied mod settings of type {type} to {_objects.PlayerData.Identifier.Incognito(null)} (Local Player)."); }); diff --git a/Glamourer/Interop/UpdateSlotService.cs b/Glamourer/Interop/UpdateSlotService.cs index e43dc74..7e5cf59 100644 --- a/Glamourer/Interop/UpdateSlotService.cs +++ b/Glamourer/Interop/UpdateSlotService.cs @@ -23,7 +23,7 @@ public readonly struct GearsetItemDataStruct [FieldOffset(16)] public readonly byte CrestBitField; // A Bitfield:: ShieldCrest == 1, HeadCrest == 2, Chest Crest == 4 [FieldOffset(17)] public readonly byte JobId; // Job ID associated with the gearset change. - // Flicks from 0 to 128 (anywhere inbetween), have yet to associate what it is linked to. Remains the same when flicking between gearsets of the same job. + // Flicks from 0 to 127 (anywhere inbetween), have yet to associate what it is linked to. Remains the same when flicking between gearsets of the same job. [FieldOffset(18)] public readonly byte UNK_18; [FieldOffset(19)] public readonly byte UNK_19; // I have never seen this be anything other than 0. @@ -56,69 +56,47 @@ public unsafe class UpdateSlotService : IDisposable { public readonly EquipSlotUpdating EquipSlotUpdatingEvent; public readonly BonusSlotUpdating BonusSlotUpdatingEvent; + public readonly GearsetDataLoaded GearsetDataLoadedEvent; private readonly DictBonusItems _bonusItems; - #region LoadAllEquipData - /////////////////////////////////////////////////// - // This is a currently undocumented signature that loads all equipment after changing a gearset. - // :: Signature Maintainers Note: - // To obtain this signature, get the stacktrace from FlagSlotForUpdate for human, and find func `sub_140842F50`. - // This function is what calls the weapon/equipment/crest loads, which call FlagSlotForUpdate if different. - // - // By detouring this function, and executing the original, then logic after, we have a consistant point in time where we know all - // slots have been flagged, meaning a consistant point in time that glamourer has processed all of its updates. - public const string LoadAllEquipmentSig = "48 89 5C 24 ?? 55 56 57 41 54 41 55 41 56 41 57 48 81 EC ?? ?? ?? ?? 48 8B 05 ?? ?? ?? ?? 48 33 C4 48 89 84 24 ?? ?? ?? ?? 44 0F B6 B9"; - private delegate Int64 LoadAllEquipmentDelegate(DrawDataContainer* drawDataContainer, GearsetItemDataStruct* gearsetData); - private Int64 LoadAllEquipmentDetour(DrawDataContainer* drawDataContainer, GearsetItemDataStruct* gearsetData) + // This function is what calls the weapon/equipment/crest loads, which call FlagSlotForUpdate if different. (MetaData not included) + public const string LoadGearsetDataSig = "48 89 5C 24 ?? 55 56 57 41 54 41 55 41 56 41 57 48 81 EC ?? ?? ?? ?? 48 8B 05 ?? ?? ?? ?? 48 33 C4 48 89 84 24 ?? ?? ?? ?? 44 0F B6 B9"; + private delegate Int64 LoadGearsetDataDelegate(DrawDataContainer* drawDataContainer, GearsetItemDataStruct* gearsetData); + private Int64 LoadGearsetDataDetour(DrawDataContainer* drawDataContainer, GearsetItemDataStruct* gearsetData) { - // return original first so we can log the changes after - var ret = _loadAllEquipmentHook.Original(drawDataContainer, gearsetData); + // Let the gearset data process all of its loads and slot flag update calls first. + var ret = _loadGearsetDataHook.Original(drawDataContainer, gearsetData); + // Ensure that the owner of the drawdata container is a character base. + Model ownerDrawObject = drawDataContainer->OwnerObject->DrawObject; + if (!ownerDrawObject.IsCharacterBase) + return ret; - // perform logic stuff. - var owner = drawDataContainer->OwnerObject; - Glamourer.Log.Warning($"[LoadAllEquipmentDetour] Owner: 0x{(nint)owner->DrawObject:X} Finished Applying its GameState!"); - Glamourer.Log.Warning($"[LoadAllEquipmentDetour] GearsetItemData: {FormatGearsetItemDataStruct(*gearsetData)}"); - - // return original. + // invoke the changed event for the state listener and return. + Glamourer.Log.Verbose($"[LoadAllEquipmentDetour] Owner: 0x{ownerDrawObject.Address:X} Finished Applying its GameState!"); + // Glamourer.Log.Verbose($"[LoadAllEquipmentDetour] GearsetItemData: {FormatGearsetItemDataStruct(*gearsetData)}"); + GearsetDataLoadedEvent.Invoke(drawDataContainer->OwnerObject->DrawObject); return ret; } - private string FormatWeaponModelId(WeaponModelId weaponModelId) => $"Id: {weaponModelId.Id}, Type: {weaponModelId.Type}, Variant: {weaponModelId.Variant}, Stain0: {weaponModelId.Stain0}, Stain1: {weaponModelId.Stain1}"; - - private string FormatGearsetItemDataStruct(GearsetItemDataStruct gearsetItemData) - { - string ret = $"\nMainhandWeaponData: {FormatWeaponModelId(gearsetItemData.MainhandWeaponData)}," + - $"\nOffhandWeaponData: {FormatWeaponModelId(gearsetItemData.OffhandWeaponData)}," + - $"\nCrestBitField: {gearsetItemData.CrestBitField} | JobId: {gearsetItemData.JobId} | UNK_18: {gearsetItemData.UNK_18} | UNK_19: {gearsetItemData.UNK_19}"; - // Iterate through offsets from 20 to 60 and format the CharacterArmor data - for (int offset = 20; offset <= 56; offset += sizeof(LegacyCharacterArmor)) - { - LegacyCharacterArmor* equipSlotPtr = (LegacyCharacterArmor*)((byte*)&gearsetItemData + offset); - int dyeOffset = (offset - 20) / sizeof(LegacyCharacterArmor) + 60; // Calculate the corresponding dye offset - byte* dyePtr = (byte*)&gearsetItemData + dyeOffset; - ret += $"\nEquipSlot {((EquipSlot)(dyeOffset-60)).ToString()}:: Id: {(*equipSlotPtr).Set}, Variant: {(*equipSlotPtr).Variant}, Stain0: {(*equipSlotPtr).Stain.Id}, Stain1: {*dyePtr}"; - } - return ret; - } -#endregion LoadAllEquipData - - public UpdateSlotService(EquipSlotUpdating equipSlotUpdating, BonusSlotUpdating bonusSlotUpdating, IGameInteropProvider interop, - DictBonusItems bonusItems) + public UpdateSlotService(EquipSlotUpdating equipSlotUpdating, BonusSlotUpdating bonusSlotUpdating, GearsetDataLoaded gearsetDataLoaded, + IGameInteropProvider interop, DictBonusItems bonusItems) { EquipSlotUpdatingEvent = equipSlotUpdating; BonusSlotUpdatingEvent = bonusSlotUpdating; + GearsetDataLoadedEvent = gearsetDataLoaded; + _bonusItems = bonusItems; interop.InitializeFromAttributes(this); _flagSlotForUpdateHook.Enable(); _flagBonusSlotForUpdateHook.Enable(); - _loadAllEquipmentHook.Enable(); + _loadGearsetDataHook.Enable(); } public void Dispose() { _flagSlotForUpdateHook.Dispose(); _flagBonusSlotForUpdateHook.Dispose(); - _loadAllEquipmentHook.Dispose(); + _loadGearsetDataHook.Dispose(); } public void UpdateEquipSlot(Model drawObject, EquipSlot slot, CharacterArmor data) @@ -167,18 +145,16 @@ public unsafe class UpdateSlotService : IDisposable [Signature(Sigs.FlagBonusSlotForUpdate, DetourName = nameof(FlagBonusSlotForUpdateDetour))] private readonly Hook _flagBonusSlotForUpdateHook = null!; - [Signature(LoadAllEquipmentSig, DetourName = nameof(LoadAllEquipmentDetour))] - private readonly Hook _loadAllEquipmentHook = null!; + [Signature(LoadGearsetDataSig, DetourName = nameof(LoadGearsetDataDetour))] + private readonly Hook _loadGearsetDataHook = null!; private ulong FlagSlotForUpdateDetour(nint drawObject, uint slotIdx, CharacterArmor* data) { var slot = slotIdx.ToEquipSlot(); var returnValue = ulong.MaxValue; - EquipSlotUpdatingEvent.Invoke(drawObject, slot, ref *data, ref returnValue); - Glamourer.Log.Information($"[FlagSlotForUpdate] Called with 0x{drawObject:X} for slot {slot} with {*data} ({returnValue:X})."); + Glamourer.Log.Excessive($"[FlagSlotForUpdate] Called with 0x{drawObject:X} for slot {slot} with {*data} ({returnValue:X})."); returnValue = returnValue == ulong.MaxValue ? _flagSlotForUpdateHook.Original(drawObject, slotIdx, data) : returnValue; - return returnValue; } @@ -186,17 +162,35 @@ public unsafe class UpdateSlotService : IDisposable { var slot = slotIdx.ToBonusSlot(); var returnValue = ulong.MaxValue; - BonusSlotUpdatingEvent.Invoke(drawObject, slot, ref *data, ref returnValue); - Glamourer.Log.Information($"[FlagBonusSlotForUpdate] Called with 0x{drawObject:X} for slot {slot} with {*data} ({returnValue:X})."); + Glamourer.Log.Excessive($"[FlagBonusSlotForUpdate] Called with 0x{drawObject:X} for slot {slot} with {*data} ({returnValue:X})."); returnValue = returnValue == ulong.MaxValue ? _flagBonusSlotForUpdateHook.Original(drawObject, slotIdx, data) : returnValue; - return returnValue; } private ulong FlagSlotForUpdateInterop(Model drawObject, EquipSlot slot, CharacterArmor armor) { - Glamourer.Log.Warning($"Glamour-Invoked Equip Slot update for 0x{drawObject.Address:X} with {slot} and {armor}."); + Glamourer.Log.Excessive($"[FlagBonusSlotForUpdate] Invoked by Glamourer on 0x{drawObject.Address:X} on {slot} with itemdata {armor}."); return _flagSlotForUpdateHook.Original(drawObject.Address, slot.ToIndex(), &armor); } + + // If you ever care to debug this, here is a formatted string output of this new gearsetDataPacket struct. + private string FormatGearsetItemDataStruct(GearsetItemDataStruct gearsetData) + { + string ret = + $"\nMainhandWeaponData: Id: {gearsetData.MainhandWeaponData.Id}, Type: {gearsetData.MainhandWeaponData.Type}, " + + $"Variant: {gearsetData.MainhandWeaponData.Variant}, Stain0: {gearsetData.MainhandWeaponData.Stain0}, Stain1: {gearsetData.MainhandWeaponData.Stain1}" + + $"\nOffhandWeaponData: Id: {gearsetData.OffhandWeaponData.Id}, Type: {gearsetData.OffhandWeaponData.Type}, " + + $"Variant: {gearsetData.OffhandWeaponData.Variant}, Stain0: {gearsetData.OffhandWeaponData.Stain0}, Stain1: {gearsetData.OffhandWeaponData.Stain1}" + + $"\nCrestBitField: {gearsetData.CrestBitField} | JobId: {gearsetData.JobId} | UNK_18: {gearsetData.UNK_18} | UNK_19: {gearsetData.UNK_19}"; + // Iterate through offsets from 20 to 60 and format the CharacterArmor data + for (int offset = 20; offset <= 56; offset += sizeof(LegacyCharacterArmor)) + { + LegacyCharacterArmor* equipSlotPtr = (LegacyCharacterArmor*)((byte*)&gearsetData + offset); + int dyeOffset = (offset - 20) / sizeof(LegacyCharacterArmor) + 60; // Calculate the corresponding dye offset + byte* dyePtr = (byte*)&gearsetData + dyeOffset; + ret += $"\nEquipSlot {((EquipSlot)(dyeOffset - 60)).ToString()}:: Id: {(*equipSlotPtr).Set}, Variant: {(*equipSlotPtr).Variant}, Stain0: {(*equipSlotPtr).Stain.Id}, Stain1: {*dyePtr}"; + } + return ret; + } } diff --git a/Glamourer/Services/CommandService.cs b/Glamourer/Services/CommandService.cs index 10f68ee..a869a09 100644 --- a/Glamourer/Services/CommandService.cs +++ b/Glamourer/Services/CommandService.cs @@ -329,7 +329,7 @@ public class CommandService : IDisposable, IApiService if (_stateManager.GetOrCreate(identifier, actor, out var state)) { _autoDesignApplier.ReapplyAutomation(actor, identifier, state, revert, out var forcedRedraw); - _stateManager.ReapplyState(actor, forcedRedraw, StateSource.Manual); + _stateManager.ReapplyAutomationState(actor, forcedRedraw, revert, StateSource.Manual); } } } @@ -378,7 +378,7 @@ public class CommandService : IDisposable, IApiService return true; foreach (var actor in data.Objects) - _stateManager.ReapplyState(actor, false, StateSource.Manual); + _stateManager.ReapplyState(actor, false, StateSource.Manual, true); } @@ -668,7 +668,7 @@ public class CommandService : IDisposable, IApiService if (!_objects.TryGetValue(identifier, out var actors)) { if (_stateManager.TryGetValue(identifier, out var state)) - _stateManager.ApplyDesign(state, design, ApplySettings.ManualWithLinks); + _stateManager.ApplyDesign(state, design, ApplySettings.ManualWithLinks with { SendStateUpdate = true }); } else { @@ -677,7 +677,7 @@ public class CommandService : IDisposable, IApiService if (_stateManager.GetOrCreate(actor.GetIdentifier(_actors), actor, out var state)) { ApplyModSettings(design, actor, applyMods); - _stateManager.ApplyDesign(state, design, ApplySettings.ManualWithLinks); + _stateManager.ApplyDesign(state, design, ApplySettings.ManualWithLinks with { SendStateUpdate = true }); } } } diff --git a/Glamourer/State/StateEditor.cs b/Glamourer/State/StateEditor.cs index b592b65..e1bd6a4 100644 --- a/Glamourer/State/StateEditor.cs +++ b/Glamourer/State/StateEditor.cs @@ -17,6 +17,7 @@ public class StateEditor( InternalStateEditor editor, StateApplier applier, StateChanged stateChanged, + StateUpdated stateUpdated, JobChangeState jobChange, Configuration config, ItemManager items, @@ -27,6 +28,7 @@ public class StateEditor( protected readonly InternalStateEditor Editor = editor; protected readonly StateApplier Applier = applier; protected readonly StateChanged StateChanged = stateChanged; + protected readonly StateUpdated StateUpdated = stateUpdated; protected readonly Configuration Config = config; protected readonly ItemManager Items = items; @@ -41,6 +43,7 @@ public class StateEditor( Glamourer.Log.Verbose( $"Set model id in state {state.Identifier.Incognito(null)} from {old} to {modelId}. [Affecting {actors.ToLazyString("nothing")}.]"); StateChanged.Invoke(StateChangeType.Model, source, state, actors, null); + StateUpdated.Invoke(StateUpdateType.ModelChange, actors); } /// @@ -419,6 +422,8 @@ public class StateEditor( Glamourer.Log.Debug($"Applied design to {state.Identifier.Incognito(null)}. [Affecting {actors.ToLazyString("nothing")}.]"); StateChanged.Invoke(StateChangeType.Design, state.Sources[MetaIndex.Wetness], state, actors, null); // FIXME: maybe later + if(settings.SendStateUpdate) + StateUpdated.Invoke(StateUpdateType.DesignApplied, actors); return; diff --git a/Glamourer/State/StateListener.cs b/Glamourer/State/StateListener.cs index 382e488..4d10c49 100644 --- a/Glamourer/State/StateListener.cs +++ b/Glamourer/State/StateListener.cs @@ -14,6 +14,7 @@ using Penumbra.GameData.DataContainers; using Glamourer.Designs; using Penumbra.GameData.Interop; using ObjectManager = Glamourer.Interop.ObjectManager; +using Glamourer.Api.Enums; namespace Glamourer.State; @@ -34,10 +35,12 @@ public class StateListener : IDisposable private readonly PenumbraService _penumbra; private readonly EquipSlotUpdating _equipSlotUpdating; private readonly BonusSlotUpdating _bonusSlotUpdating; + private readonly GearsetDataLoaded _gearsetDataLoaded; private readonly WeaponLoading _weaponLoading; private readonly HeadGearVisibilityChanged _headGearVisibility; private readonly VisorStateChanged _visorState; private readonly WeaponVisibilityChanged _weaponVisibility; + private readonly StateUpdated _stateUpdated; private readonly AutoDesignApplier _autoDesignApplier; private readonly FunModule _funModule; private readonly HumanModelList _humans; @@ -54,11 +57,11 @@ public class StateListener : IDisposable private ActorState? _customizeState; public StateListener(StateManager manager, ItemManager items, PenumbraService penumbra, ActorManager actors, Configuration config, - EquipSlotUpdating equipSlotUpdating, WeaponLoading weaponLoading, VisorStateChanged visorState, + EquipSlotUpdating equipSlotUpdating, GearsetDataLoaded gearsetDataLoaded, WeaponLoading weaponLoading, VisorStateChanged visorState, WeaponVisibilityChanged weaponVisibility, HeadGearVisibilityChanged headGearVisibility, AutoDesignApplier autoDesignApplier, FunModule funModule, HumanModelList humans, StateApplier applier, MovedEquipment movedEquipment, ObjectManager objects, GPoseService gPose, ChangeCustomizeService changeCustomizeService, CustomizeService customizations, ICondition condition, - CrestService crestService, BonusSlotUpdating bonusSlotUpdating) + CrestService crestService, BonusSlotUpdating bonusSlotUpdating, StateUpdated stateUpdated) { _manager = manager; _items = items; @@ -66,6 +69,7 @@ public class StateListener : IDisposable _actors = actors; _config = config; _equipSlotUpdating = equipSlotUpdating; + _gearsetDataLoaded = gearsetDataLoaded; _weaponLoading = weaponLoading; _visorState = visorState; _weaponVisibility = weaponVisibility; @@ -82,6 +86,7 @@ public class StateListener : IDisposable _condition = condition; _crestService = crestService; _bonusSlotUpdating = bonusSlotUpdating; + _stateUpdated = stateUpdated; Subscribe(); } @@ -259,6 +264,22 @@ public class StateListener : IDisposable } } + private void OnGearsetDataLoaded(Model model) + { + var actor = _penumbra.GameObjectFromDrawObject(model); + if (_condition[ConditionFlag.CreatingCharacter] && actor.Index >= ObjectIndex.CutsceneStart) + return; + + // ensure actor and state are valid. + if (!actor.Identifier(_actors, out var identifier)) + return; + + _objects.Update(); + if (_objects.TryGetValue(identifier, out var actors) && actors.Valid) + _stateUpdated.Invoke(StateUpdateType.Gearset, actors); + } + + private void OnMovedEquipment((EquipSlot, uint, StainIds)[] items) { _objects.Update(); @@ -382,7 +403,7 @@ public class StateListener : IDisposable lastFistOffhand = new CharacterWeapon((PrimaryId)(weapon.Skeleton.Id + 50), weapon.Weapon, weapon.Variant, weapon.Stains); _fistOffhands[actor] = lastFistOffhand; - Glamourer.Log.Verbose($"Storing fist weapon offhand {lastFistOffhand} for 0x{actor.Address:X}."); + Glamourer.Log.Excessive($"Storing fist weapon offhand {lastFistOffhand} for 0x{actor.Address:X}."); } _funModule.ApplyFunToWeapon(actor, ref weapon, slot); @@ -765,6 +786,7 @@ public class StateListener : IDisposable _penumbra.CreatedCharacterBase += OnCreatedCharacterBase; _equipSlotUpdating.Subscribe(OnEquipSlotUpdating, EquipSlotUpdating.Priority.StateListener); _bonusSlotUpdating.Subscribe(OnBonusSlotUpdating, BonusSlotUpdating.Priority.StateListener); + _gearsetDataLoaded.Subscribe(OnGearsetDataLoaded, GearsetDataLoaded.Priority.StateListener); _movedEquipment.Subscribe(OnMovedEquipment, MovedEquipment.Priority.StateListener); _weaponLoading.Subscribe(OnWeaponLoading, WeaponLoading.Priority.StateListener); _visorState.Subscribe(OnVisorChange, VisorStateChanged.Priority.StateListener); @@ -782,6 +804,7 @@ public class StateListener : IDisposable _penumbra.CreatedCharacterBase -= OnCreatedCharacterBase; _equipSlotUpdating.Unsubscribe(OnEquipSlotUpdating); _bonusSlotUpdating.Unsubscribe(OnBonusSlotUpdating); + _gearsetDataLoaded.Unsubscribe(OnGearsetDataLoaded); _movedEquipment.Unsubscribe(OnMovedEquipment); _weaponLoading.Unsubscribe(OnWeaponLoading); _visorState.Unsubscribe(OnVisorChange); diff --git a/Glamourer/State/StateManager.cs b/Glamourer/State/StateManager.cs index 736dd6e..129f8bb 100644 --- a/Glamourer/State/StateManager.cs +++ b/Glamourer/State/StateManager.cs @@ -21,6 +21,7 @@ public sealed class StateManager( ActorManager _actors, ItemManager items, StateChanged @event, + StateUpdated @event2, StateApplier applier, InternalStateEditor editor, HumanModelList _humans, @@ -30,7 +31,7 @@ public sealed class StateManager( DesignMerger merger, ModSettingApplier modApplier, GPoseService gPose) - : StateEditor(editor, applier, @event, jobChange, config, items, merger, modApplier, gPose), + : StateEditor(editor, applier, @event, @event2, jobChange, config, items, merger, modApplier, gPose), IReadOnlyDictionary { private readonly Dictionary _states = []; @@ -235,7 +236,7 @@ public sealed class StateManager( public void TurnHuman(ActorState state, StateSource source, uint key = 0) => ChangeModelId(state, 0, CustomizeArray.Default, nint.Zero, source, key); - public void ResetState(ActorState state, StateSource source, uint key = 0) + public void ResetState(ActorState state, StateSource source, uint key = 0, bool stateUpdate = false) { if (!state.Unlock(key)) return; @@ -276,6 +277,9 @@ public sealed class StateManager( Glamourer.Log.Debug( $"Reset entire state of {state.Identifier.Incognito(null)} to game base. [Affecting {actors.ToLazyString("nothing")}.]"); StateChanged.Invoke(StateChangeType.Reset, source, state, actors, null); + // only invoke if we define this reset call as the final call in our state update. + if(stateUpdate) + StateUpdated.Invoke(StateUpdateType.Revert, actors); } public void ResetAdvancedState(ActorState state, StateSource source, uint key = 0) @@ -301,6 +305,8 @@ public sealed class StateManager( Glamourer.Log.Debug( $"Reset advanced customization and dye state of {state.Identifier.Incognito(null)} to game base. [Affecting {actors.ToLazyString("nothing")}.]"); StateChanged.Invoke(StateChangeType.Reset, source, state, actors, null); + // Update that we have completed a full operation. (We can do this directly as nothing else is linked) + StateUpdated.Invoke(StateUpdateType.RevertAdvanced, actors); } public void ResetCustomize(ActorState state, StateSource source, uint key = 0) @@ -318,6 +324,8 @@ public sealed class StateManager( actors = Applier.ChangeCustomize(state, true); Glamourer.Log.Verbose( $"Reset customization state of {state.Identifier.Incognito(null)} to game base. [Affecting {actors.ToLazyString("nothing")}.]"); + // Update that we have completed a full operation. (We can do this directly as nothing else is linked) + StateUpdated.Invoke(StateUpdateType.RevertCustomize, actors); } public void ResetEquip(ActorState state, StateSource source, uint key = 0) @@ -367,6 +375,8 @@ public sealed class StateManager( Glamourer.Log.Verbose( $"Reset equipment state of {state.Identifier.Incognito(null)} to game base. [Affecting {actors.ToLazyString("nothing")}.]"); + // Update that we have completed a full operation. (We can do this directly as nothing else is linked) + StateUpdated.Invoke(StateUpdateType.RevertEquipment, actors); } public void ResetStateFixed(ActorState state, bool respectManualPalettes, uint key = 0) @@ -443,21 +453,44 @@ public sealed class StateManager( } } - public void ReapplyState(Actor actor, bool forceRedraw, StateSource source) + public void ReapplyState(Actor actor, bool forceRedraw, StateSource source, bool isUpdate = false) { if (!GetOrCreate(actor, out var state)) return; - ReapplyState(actor, state, forceRedraw, source); + ReapplyState(actor, state, forceRedraw, source, isUpdate); } - public void ReapplyState(Actor actor, ActorState state, bool forceRedraw, StateSource source) + public void ReapplyState(Actor actor, ActorState state, bool forceRedraw, StateSource source, bool isUpdate) { var data = Applier.ApplyAll(state, forceRedraw || !actor.Model.IsHuman || CustomizeArray.Compare(actor.Model.GetCustomize(), state.ModelData.Customize).RequiresRedraw(), false); StateChanged.Invoke(StateChangeType.Reapply, source, state, data, null); + if(isUpdate) + StateUpdated.Invoke(StateUpdateType.Reapply, data); + } + + /// Automation variant for reapply, to fire the correct StateUpdateType once reapplied. + public void ReapplyAutomationState(Actor actor, bool forceRedraw, bool wasReset, StateSource source) + { + if (!GetOrCreate(actor, out var state)) + return; + + ReapplyAutomationState(actor, state, forceRedraw, wasReset, source); + } + + /// Automation variant for reapply, to fire the correct StateUpdateType once reapplied. + public void ReapplyAutomationState(Actor actor, ActorState state, bool forceRedraw, bool wasReset, StateSource source) + { + var data = Applier.ApplyAll(state, + forceRedraw + || !actor.Model.IsHuman + || CustomizeArray.Compare(actor.Model.GetCustomize(), state.ModelData.Customize).RequiresRedraw(), false); + StateChanged.Invoke(StateChangeType.Reapply, source, state, data, null); + // invoke the automation update based on what reset is. + StateUpdated.Invoke(wasReset ? StateUpdateType.RevertAutomation : StateUpdateType.ReapplyAutomation, data); } public void DeleteState(ActorIdentifier identifier) From 8b609e5f0506e4d64f843631e47ecc6adcd10e90 Mon Sep 17 00:00:00 2001 From: Cordelia Mist Date: Fri, 17 Jan 2025 18:09:10 -0800 Subject: [PATCH 03/10] corrected comments and formatting to reflect Glamourer's main branch. --- Glamourer/Automation/AutoDesignApplier.cs | 9 ---- Glamourer/Events/GearsetDataLoaded.cs | 4 +- Glamourer/Events/StateUpdated.cs | 4 +- Glamourer/Interop/ChangeCustomizeService.cs | 3 -- Glamourer/Interop/InventoryService.cs | 38 +++++++-------- Glamourer/Interop/UpdateSlotService.cs | 51 ++++++++++----------- Glamourer/State/StateEditor.cs | 25 +++++----- Glamourer/State/StateListener.cs | 2 +- Glamourer/State/StateManager.cs | 12 ++--- 9 files changed, 64 insertions(+), 84 deletions(-) diff --git a/Glamourer/Automation/AutoDesignApplier.cs b/Glamourer/Automation/AutoDesignApplier.cs index bcc907c..52956cc 100644 --- a/Glamourer/Automation/AutoDesignApplier.cs +++ b/Glamourer/Automation/AutoDesignApplier.cs @@ -202,17 +202,8 @@ public sealed class AutoDesignApplier : IDisposable } } - /// - /// JOB CHANGE IS CALLED UPON HERE. - /// private void OnJobChange(Actor actor, Job oldJob, Job newJob) { - unsafe - { - var drawObject = actor.AsCharacter->DrawObject; - Glamourer.Log.Information($"[AutoDesignApplier][OnJobChange] 0x{(nint)drawObject:X} changed job from {oldJob} ({oldJob.Id}) to {newJob} ({newJob.Id})."); - } - if (!_config.EnableAutoDesigns || !actor.Identifier(_actors, out var id)) return; diff --git a/Glamourer/Events/GearsetDataLoaded.cs b/Glamourer/Events/GearsetDataLoaded.cs index 47d0108..680ae3f 100644 --- a/Glamourer/Events/GearsetDataLoaded.cs +++ b/Glamourer/Events/GearsetDataLoaded.cs @@ -1,7 +1,5 @@ using OtterGui.Classes; -using Penumbra.GameData.Enums; using Penumbra.GameData.Interop; -using Penumbra.GameData.Structs; namespace Glamourer.Events; @@ -9,7 +7,7 @@ namespace Glamourer.Events; /// Triggers when the equipped gearset finished running all of its LoadEquipment, LoadWeapon, and crest calls. /// This defines a universal endpoint of base game state application to monitor. /// -/// The model drawobject associated with the finished load (should always be ClientPlayer) +/// The model drawobject associated with the finished load (Also fired by other players on render) /// /// public sealed class GearsetDataLoaded() diff --git a/Glamourer/Events/StateUpdated.cs b/Glamourer/Events/StateUpdated.cs index 82d737f..f18a69a 100644 --- a/Glamourer/Events/StateUpdated.cs +++ b/Glamourer/Events/StateUpdated.cs @@ -9,10 +9,8 @@ namespace Glamourer.Events; /// /// Triggered when a Design is edited in any way. /// -/// Parameter is the type of the change -/// Parameter is the changed saved state. +/// Parameter is the operation that finished updating the saved state. /// Parameter is the existing actors using this saved state. -/// Parameter is any additional data depending on the type of change. /// /// public sealed class StateUpdated() diff --git a/Glamourer/Interop/ChangeCustomizeService.cs b/Glamourer/Interop/ChangeCustomizeService.cs index 10e3a12..495d69c 100644 --- a/Glamourer/Interop/ChangeCustomizeService.cs +++ b/Glamourer/Interop/ChangeCustomizeService.cs @@ -64,7 +64,6 @@ public unsafe class ChangeCustomizeService : EventWrapperRef2 _changeCustomizeHook; - // manual invoke by calling the detours _original call to `execute to` instead of `listen to`. public bool UpdateCustomize(Model model, CustomizeArray customize) { if (!model.IsHuman) @@ -79,7 +78,6 @@ public unsafe class ChangeCustomizeService : EventWrapperRef2 UpdateCustomize(actor.Model, customize); - // detoured method. private bool ChangeCustomizeDetour(Human* human, byte* data, byte skipEquipment) { if (!InUpdate.InMethod) @@ -90,7 +88,6 @@ public unsafe class ChangeCustomizeService : EventWrapperRef2 action, Post.Priority priority) => _postEvent.Subscribe(action, priority); diff --git a/Glamourer/Interop/InventoryService.cs b/Glamourer/Interop/InventoryService.cs index 743bea1..33ba5cf 100644 --- a/Glamourer/Interop/InventoryService.cs +++ b/Glamourer/Interop/InventoryService.cs @@ -5,7 +5,6 @@ using FFXIVClientStructs.FFXIV.Client.Game; using FFXIVClientStructs.FFXIV.Client.UI.Misc; using Glamourer.Events; using OtterGui.Services; -using Penumbra.GameData; using Penumbra.GameData.Enums; using Penumbra.GameData.Structs; using Penumbra.String; @@ -14,10 +13,6 @@ namespace Glamourer.Interop; public sealed unsafe class InventoryService : IDisposable, IRequiredService { - private readonly MovedEquipment _movedItemsEvent; - private readonly EquippedGearset _gearsetEvent; - private readonly List<(EquipSlot, uint, StainIds)> _itemList = new(12); - // Called by EquipGearset, but returns a pointer instead of an int. // This is the internal function processed by all sources of Equipping a gearset, // such as hotbar gearset application and command gearset application @@ -27,10 +22,16 @@ public sealed unsafe class InventoryService : IDisposable, IRequiredService [Signature(EquipGearsetInternal, DetourName = nameof(EquipGearSetInternalDetour))] private readonly Hook _equipGearsetInternalHook = null!; + // The following above is currently pending for an accepted PR in FFXIVCLientStructs. + // Once accepted, remove everything above this comment and replace EquipGearset with EquipGearsetInternal. + + private readonly MovedEquipment _movedItemsEvent; + private readonly EquippedGearset _gearsetEvent; + private readonly List<(EquipSlot, uint, StainIds)> _itemList = new(12); public InventoryService(MovedEquipment movedItemsEvent, IGameInteropProvider interop, EquippedGearset gearsetEvent) { _movedItemsEvent = movedItemsEvent; - _gearsetEvent = gearsetEvent; + _gearsetEvent = gearsetEvent; _moveItemHook = interop.HookFromAddress((nint)InventoryManager.MemberFunctionPointers.MoveItemSlot, MoveItemDetour); _equipGearsetHook = interop.HookFromAddress((nint)RaptureGearsetModule.MemberFunctionPointers.EquipGearset, EquipGearSetDetour); @@ -58,7 +59,7 @@ public sealed unsafe class InventoryService : IDisposable, IRequiredService var ret = _equipGearsetInternalHook.Original(module, gearsetId, glamourPlateId); var set = module->GetGearset((int)gearsetId); _gearsetEvent.Invoke(new ByteString(set->Name).ToString(), (int)gearsetId, prior, glamourPlateId, set->ClassJob); - Glamourer.Log.Verbose($"[InventoryService] [EquipInternal] Applied gear set {gearsetId} with glamour plate {glamourPlateId} (Returned {ret})"); + Glamourer.Log.Verbose($"[InventoryService] Applied gear set {gearsetId} with glamour plate {glamourPlateId} (Returned {ret})"); if (ret == 0) { var entry = module->GetGearset((int)gearsetId); @@ -131,9 +132,10 @@ public sealed unsafe class InventoryService : IDisposable, IRequiredService return ret; } + // Remove once internal is added. This no longer serves any purpose. private int EquipGearSetDetour(RaptureGearsetModule* module, int gearsetId, byte glamourPlateId) { - var ret = _equipGearsetHook.Original(module, gearsetId, glamourPlateId); + var ret = _equipGearsetHook.Original(module, gearsetId, glamourPlateId); Glamourer.Log.Excessive($"[InventoryService] (old) Applied gear set {gearsetId} with glamour plate {glamourPlateId} (Returned {ret})"); return ret; } @@ -216,18 +218,18 @@ public sealed unsafe class InventoryService : IDisposable, IRequiredService private static EquipSlot GetSlot(uint slot) => slot switch { - 0 => EquipSlot.MainHand, - 1 => EquipSlot.OffHand, - 2 => EquipSlot.Head, - 3 => EquipSlot.Body, - 4 => EquipSlot.Hands, - 6 => EquipSlot.Legs, - 7 => EquipSlot.Feet, - 8 => EquipSlot.Ears, - 9 => EquipSlot.Neck, + 0 => EquipSlot.MainHand, + 1 => EquipSlot.OffHand, + 2 => EquipSlot.Head, + 3 => EquipSlot.Body, + 4 => EquipSlot.Hands, + 6 => EquipSlot.Legs, + 7 => EquipSlot.Feet, + 8 => EquipSlot.Ears, + 9 => EquipSlot.Neck, 10 => EquipSlot.Wrists, 11 => EquipSlot.RFinger, 12 => EquipSlot.LFinger, - _ => EquipSlot.Unknown, + _ => EquipSlot.Unknown, }; } diff --git a/Glamourer/Interop/UpdateSlotService.cs b/Glamourer/Interop/UpdateSlotService.cs index 7e5cf59..9ee8d8f 100644 --- a/Glamourer/Interop/UpdateSlotService.cs +++ b/Glamourer/Interop/UpdateSlotService.cs @@ -8,12 +8,11 @@ using Penumbra.GameData.DataContainers; using Penumbra.GameData.Enums; using Penumbra.GameData.Interop; using Penumbra.GameData.Structs; + namespace Glamourer.Interop; -/// -/// This struct is the struct that loadallequipment passes in as its gearsetData container. -/// -[StructLayout(LayoutKind.Explicit)] // Size of 70 bytes maybe? +// This struct is implemented into a PR for FFXIVClientStructs. Once merged, remove this struct and reference the data in ClientStructs instead. +[StructLayout(LayoutKind.Explicit)] public readonly struct GearsetItemDataStruct { // Stores the weapon data. Includes both dyes in the data. @@ -54,30 +53,15 @@ public readonly struct GearsetItemDataStruct public unsafe class UpdateSlotService : IDisposable { + // This function is what calls the weapon/equipment/crest loads, which call FlagSlotForUpdate if different. (MetaData not included) + public const string LoadGearsetDataSig = "48 89 5C 24 ?? 55 56 57 41 54 41 55 41 56 41 57 48 81 EC ?? ?? ?? ?? 48 8B 05 ?? ?? ?? ?? 48 33 C4 48 89 84 24 ?? ?? ?? ?? 44 0F B6 B9"; + private delegate Int64 LoadGearsetDataDelegate(DrawDataContainer* drawDataContainer, GearsetItemDataStruct* gearsetData); + // The above can be removed after the FFXIVClientStruct Merge is made! + public readonly EquipSlotUpdating EquipSlotUpdatingEvent; public readonly BonusSlotUpdating BonusSlotUpdatingEvent; public readonly GearsetDataLoaded GearsetDataLoadedEvent; private readonly DictBonusItems _bonusItems; - - // This function is what calls the weapon/equipment/crest loads, which call FlagSlotForUpdate if different. (MetaData not included) - public const string LoadGearsetDataSig = "48 89 5C 24 ?? 55 56 57 41 54 41 55 41 56 41 57 48 81 EC ?? ?? ?? ?? 48 8B 05 ?? ?? ?? ?? 48 33 C4 48 89 84 24 ?? ?? ?? ?? 44 0F B6 B9"; - private delegate Int64 LoadGearsetDataDelegate(DrawDataContainer* drawDataContainer, GearsetItemDataStruct* gearsetData); - private Int64 LoadGearsetDataDetour(DrawDataContainer* drawDataContainer, GearsetItemDataStruct* gearsetData) - { - // Let the gearset data process all of its loads and slot flag update calls first. - var ret = _loadGearsetDataHook.Original(drawDataContainer, gearsetData); - // Ensure that the owner of the drawdata container is a character base. - Model ownerDrawObject = drawDataContainer->OwnerObject->DrawObject; - if (!ownerDrawObject.IsCharacterBase) - return ret; - - // invoke the changed event for the state listener and return. - Glamourer.Log.Verbose($"[LoadAllEquipmentDetour] Owner: 0x{ownerDrawObject.Address:X} Finished Applying its GameState!"); - // Glamourer.Log.Verbose($"[LoadAllEquipmentDetour] GearsetItemData: {FormatGearsetItemDataStruct(*gearsetData)}"); - GearsetDataLoadedEvent.Invoke(drawDataContainer->OwnerObject->DrawObject); - return ret; - } - public UpdateSlotService(EquipSlotUpdating equipSlotUpdating, BonusSlotUpdating bonusSlotUpdating, GearsetDataLoaded gearsetDataLoaded, IGameInteropProvider interop, DictBonusItems bonusItems) { @@ -150,7 +134,7 @@ public unsafe class UpdateSlotService : IDisposable private ulong FlagSlotForUpdateDetour(nint drawObject, uint slotIdx, CharacterArmor* data) { - var slot = slotIdx.ToEquipSlot(); + var slot = slotIdx.ToEquipSlot(); var returnValue = ulong.MaxValue; EquipSlotUpdatingEvent.Invoke(drawObject, slot, ref *data, ref returnValue); Glamourer.Log.Excessive($"[FlagSlotForUpdate] Called with 0x{drawObject:X} for slot {slot} with {*data} ({returnValue:X})."); @@ -160,7 +144,7 @@ public unsafe class UpdateSlotService : IDisposable private ulong FlagBonusSlotForUpdateDetour(nint drawObject, uint slotIdx, CharacterArmor* data) { - var slot = slotIdx.ToBonusSlot(); + var slot = slotIdx.ToBonusSlot(); var returnValue = ulong.MaxValue; BonusSlotUpdatingEvent.Invoke(drawObject, slot, ref *data, ref returnValue); Glamourer.Log.Excessive($"[FlagBonusSlotForUpdate] Called with 0x{drawObject:X} for slot {slot} with {*data} ({returnValue:X})."); @@ -173,6 +157,20 @@ public unsafe class UpdateSlotService : IDisposable Glamourer.Log.Excessive($"[FlagBonusSlotForUpdate] Invoked by Glamourer on 0x{drawObject.Address:X} on {slot} with itemdata {armor}."); return _flagSlotForUpdateHook.Original(drawObject.Address, slot.ToIndex(), &armor); } + private Int64 LoadGearsetDataDetour(DrawDataContainer* drawDataContainer, GearsetItemDataStruct* gearsetData) + { + // Let the gearset data process all of its loads and slot flag update calls first. + var ret = _loadGearsetDataHook.Original(drawDataContainer, gearsetData); + Model ownerDrawObject = drawDataContainer->OwnerObject->DrawObject; + if (!ownerDrawObject.IsCharacterBase) + return ret; + + // invoke the changed event for the state listener and return. + Glamourer.Log.Verbose($"[LoadAllEquipmentDetour] Owner: 0x{ownerDrawObject.Address:X} Finished Applying its GameState!"); + // Glamourer.Log.Verbose($"[LoadAllEquipmentDetour] GearsetItemData: {FormatGearsetItemDataStruct(*gearsetData)}"); + GearsetDataLoadedEvent.Invoke(drawDataContainer->OwnerObject->DrawObject); + return ret; + } // If you ever care to debug this, here is a formatted string output of this new gearsetDataPacket struct. private string FormatGearsetItemDataStruct(GearsetItemDataStruct gearsetData) @@ -183,7 +181,6 @@ public unsafe class UpdateSlotService : IDisposable $"\nOffhandWeaponData: Id: {gearsetData.OffhandWeaponData.Id}, Type: {gearsetData.OffhandWeaponData.Type}, " + $"Variant: {gearsetData.OffhandWeaponData.Variant}, Stain0: {gearsetData.OffhandWeaponData.Stain0}, Stain1: {gearsetData.OffhandWeaponData.Stain1}" + $"\nCrestBitField: {gearsetData.CrestBitField} | JobId: {gearsetData.JobId} | UNK_18: {gearsetData.UNK_18} | UNK_19: {gearsetData.UNK_19}"; - // Iterate through offsets from 20 to 60 and format the CharacterArmor data for (int offset = 20; offset <= 56; offset += sizeof(LegacyCharacterArmor)) { LegacyCharacterArmor* equipSlotPtr = (LegacyCharacterArmor*)((byte*)&gearsetData + offset); diff --git a/Glamourer/State/StateEditor.cs b/Glamourer/State/StateEditor.cs index e1bd6a4..891c61d 100644 --- a/Glamourer/State/StateEditor.cs +++ b/Glamourer/State/StateEditor.cs @@ -54,7 +54,7 @@ public class StateEditor( return; var actors = Applier.ChangeCustomize(state, settings.Source.RequiresChange()); - Glamourer.Log.Information( + Glamourer.Log.Verbose( $"Set {idx.ToDefaultName()} customizations in state {state.Identifier.Incognito(null)} from {old.Value} to {value.Value}. [Affecting {actors.ToLazyString("nothing")}.]"); StateChanged.Invoke(StateChangeType.Customize, settings.Source, state, actors, new CustomizeTransaction(idx, old, value)); } @@ -67,7 +67,7 @@ public class StateEditor( return; var actors = Applier.ChangeCustomize(state, settings.Source.RequiresChange()); - Glamourer.Log.Information( + Glamourer.Log.Verbose( $"Set {applied} customizations in state {state.Identifier.Incognito(null)} from {old} to {customizeInput}. [Affecting {actors.ToLazyString("nothing")}.]"); StateChanged.Invoke(StateChangeType.EntireCustomize, settings.Source, state, actors, new EntireCustomizeTransaction(applied, old, customizeInput)); @@ -78,10 +78,7 @@ public class StateEditor( { var state = (ActorState)data; if (!Editor.ChangeItem(state, slot, item, settings.Source, out var old, settings.Key)) - { - Glamourer.Log.Information("Not Setting State or invoking, Editor requested us not to change it!"); return; - } var type = slot.ToIndex() < 10 ? StateChangeType.Equip : StateChangeType.Weapon; var actors = type is StateChangeType.Equip @@ -92,8 +89,8 @@ public class StateEditor( if (slot is EquipSlot.MainHand) ApplyMainhandPeriphery(state, item, null, settings); - Glamourer.Log.Debug( - $"[ChangeItem] Set {slot.ToName()} in state {state.Identifier.Incognito(null)} from {old.Name} ({old.ItemId}) to {item.Name} ({item.ItemId}). [Affecting {actors.ToLazyString("nothing")}.]"); + Glamourer.Log.Verbose( + $"Set {slot.ToName()} in state {state.Identifier.Incognito(null)} from {old.Name} ({old.ItemId}) to {item.Name} ({item.ItemId}). [Affecting {actors.ToLazyString("nothing")}.]"); if (type is StateChangeType.Equip) { @@ -122,8 +119,8 @@ public class StateEditor( return; var actors = Applier.ChangeBonusItem(state, slot, settings.Source.RequiresChange()); - Glamourer.Log.Debug( - $"[ChangeBonus] Set {slot.ToName()} in state {state.Identifier.Incognito(null)} from {old.Name} ({old.Id}) to {item.Name} ({item.Id}). [Affecting {actors.ToLazyString("nothing")}.]"); + Glamourer.Log.Verbose( + $"Set {slot.ToName()} in state {state.Identifier.Incognito(null)} from {old.Name} ({old.Id}) to {item.Name} ({item.Id}). [Affecting {actors.ToLazyString("nothing")}.]"); StateChanged.Invoke(StateChangeType.BonusItem, settings.Source, state, actors, new BonusItemTransaction(slot, old, item)); } @@ -155,8 +152,8 @@ public class StateEditor( if (slot is EquipSlot.MainHand) ApplyMainhandPeriphery(state, item, stains, settings); - Glamourer.Log.Debug( - $"[ChangeEquip] Set {slot.ToName()} in state {state.Identifier.Incognito(null)} from {old.Name} ({old.ItemId}) to {item!.Value.Name} ({item.Value.ItemId}) and its stain from {oldStains} to {stains!.Value}. [Affecting {actors.ToLazyString("nothing")}.]"); + Glamourer.Log.Verbose( + $"Set {slot.ToName()} in state {state.Identifier.Incognito(null)} from {old.Name} ({old.ItemId}) to {item!.Value.Name} ({item.Value.ItemId}) and its stain from {oldStains} to {stains!.Value}. [Affecting {actors.ToLazyString("nothing")}.]"); if (type is StateChangeType.Equip) { StateChanged.Invoke(type, settings.Source, state, actors, new EquipTransaction(slot, old, item!.Value)); @@ -187,7 +184,7 @@ public class StateEditor( return; var actors = Applier.ChangeStain(state, slot, settings.Source.RequiresChange()); - Glamourer.Log.Debug( + Glamourer.Log.Verbose( $"Set {slot.ToName()} stain in state {state.Identifier.Incognito(null)} from {old} to {stains}. [Affecting {actors.ToLazyString("nothing")}.]"); StateChanged.Invoke(StateChangeType.Stains, settings.Source, state, actors, new StainTransaction(slot, old, stains)); } @@ -419,8 +416,8 @@ public class StateEditor( var actors = settings.Source.RequiresChange() ? Applier.ApplyAll(state, requiresRedraw, false) : ActorData.Invalid; - - Glamourer.Log.Debug($"Applied design to {state.Identifier.Incognito(null)}. [Affecting {actors.ToLazyString("nothing")}.]"); + + Glamourer.Log.Verbose($"Applied design to {state.Identifier.Incognito(null)}. [Affecting {actors.ToLazyString("nothing")}.]"); StateChanged.Invoke(StateChangeType.Design, state.Sources[MetaIndex.Wetness], state, actors, null); // FIXME: maybe later if(settings.SendStateUpdate) StateUpdated.Invoke(StateUpdateType.DesignApplied, actors); diff --git a/Glamourer/State/StateListener.cs b/Glamourer/State/StateListener.cs index 4d10c49..d312815 100644 --- a/Glamourer/State/StateListener.cs +++ b/Glamourer/State/StateListener.cs @@ -13,8 +13,8 @@ using Glamourer.GameData; using Penumbra.GameData.DataContainers; using Glamourer.Designs; using Penumbra.GameData.Interop; -using ObjectManager = Glamourer.Interop.ObjectManager; using Glamourer.Api.Enums; +using ObjectManager = Glamourer.Interop.ObjectManager; namespace Glamourer.State; diff --git a/Glamourer/State/StateManager.cs b/Glamourer/State/StateManager.cs index 129f8bb..0348148 100644 --- a/Glamourer/State/StateManager.cs +++ b/Glamourer/State/StateManager.cs @@ -274,7 +274,7 @@ public sealed class StateManager( if (source is not StateSource.Game) actors = Applier.ApplyAll(state, redraw, true); - Glamourer.Log.Debug( + Glamourer.Log.Verbose( $"Reset entire state of {state.Identifier.Incognito(null)} to game base. [Affecting {actors.ToLazyString("nothing")}.]"); StateChanged.Invoke(StateChangeType.Reset, source, state, actors, null); // only invoke if we define this reset call as the final call in our state update. @@ -302,7 +302,7 @@ public sealed class StateManager( state.Materials.Clear(); - Glamourer.Log.Debug( + Glamourer.Log.Verbose( $"Reset advanced customization and dye state of {state.Identifier.Incognito(null)} to game base. [Affecting {actors.ToLazyString("nothing")}.]"); StateChanged.Invoke(StateChangeType.Reset, source, state, actors, null); // Update that we have completed a full operation. (We can do this directly as nothing else is linked) @@ -453,22 +453,22 @@ public sealed class StateManager( } } - public void ReapplyState(Actor actor, bool forceRedraw, StateSource source, bool isUpdate = false) + public void ReapplyState(Actor actor, bool forceRedraw, StateSource source, bool stateUpdate = false) { if (!GetOrCreate(actor, out var state)) return; - ReapplyState(actor, state, forceRedraw, source, isUpdate); + ReapplyState(actor, state, forceRedraw, source, stateUpdate); } - public void ReapplyState(Actor actor, ActorState state, bool forceRedraw, StateSource source, bool isUpdate) + public void ReapplyState(Actor actor, ActorState state, bool forceRedraw, StateSource source, bool stateUpdate) { var data = Applier.ApplyAll(state, forceRedraw || !actor.Model.IsHuman || CustomizeArray.Compare(actor.Model.GetCustomize(), state.ModelData.Customize).RequiresRedraw(), false); StateChanged.Invoke(StateChangeType.Reapply, source, state, data, null); - if(isUpdate) + if(stateUpdate) StateUpdated.Invoke(StateUpdateType.Reapply, data); } From 9c57935a872dc98fa0ddfdcc81d891dada3b0054 Mon Sep 17 00:00:00 2001 From: Cordelia Mist Date: Fri, 17 Jan 2025 18:52:16 -0800 Subject: [PATCH 04/10] removed unessisary usings and corrected gearsetDataLoaded stateListener ref. --- Glamourer/Automation/AutoDesignApplier.cs | 1 - Glamourer/Events/GearsetDataLoaded.cs | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/Glamourer/Automation/AutoDesignApplier.cs b/Glamourer/Automation/AutoDesignApplier.cs index 52956cc..660acf4 100644 --- a/Glamourer/Automation/AutoDesignApplier.cs +++ b/Glamourer/Automation/AutoDesignApplier.cs @@ -1,5 +1,4 @@ using Dalamud.Plugin.Services; -using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; using FFXIVClientStructs.FFXIV.Client.UI.Misc; using Glamourer.Designs; using Glamourer.Designs.Links; diff --git a/Glamourer/Events/GearsetDataLoaded.cs b/Glamourer/Events/GearsetDataLoaded.cs index 680ae3f..dd12bc1 100644 --- a/Glamourer/Events/GearsetDataLoaded.cs +++ b/Glamourer/Events/GearsetDataLoaded.cs @@ -15,7 +15,7 @@ public sealed class GearsetDataLoaded() { public enum Priority { - /// + /// StateListener = 0, } } \ No newline at end of file From 1d185e9bfe8f9badb6b58c62f6cfda49bcefba8a Mon Sep 17 00:00:00 2001 From: Cordelia Mist Date: Sun, 19 Jan 2025 09:07:43 -0800 Subject: [PATCH 05/10] Added Proper Unsubsribe from OnStateUpdated. Ensures that ReapplyAutomation is called from OnAutomationChange when change occurs. Reformatted temporary Signature locations and comments to align with the structure of the respective classes. --- Glamourer/Api/StateApi.cs | 1 + Glamourer/Automation/AutoDesignApplier.cs | 4 +-- Glamourer/Interop/InventoryService.cs | 35 ++++++------------ Glamourer/Interop/UpdateSlotService.cs | 44 +++++++++++++---------- 4 files changed, 39 insertions(+), 45 deletions(-) diff --git a/Glamourer/Api/StateApi.cs b/Glamourer/Api/StateApi.cs index 248e294..34f4ad9 100644 --- a/Glamourer/Api/StateApi.cs +++ b/Glamourer/Api/StateApi.cs @@ -54,6 +54,7 @@ public sealed class StateApi : IGlamourerApiState, IApiService, IDisposable public void Dispose() { _stateChanged.Unsubscribe(OnStateChanged); + _stateUpdated.Unsubscribe(OnStateUpdated); _gPose.Unsubscribe(OnGPoseChange); } diff --git a/Glamourer/Automation/AutoDesignApplier.cs b/Glamourer/Automation/AutoDesignApplier.cs index 660acf4..1655c15 100644 --- a/Glamourer/Automation/AutoDesignApplier.cs +++ b/Glamourer/Automation/AutoDesignApplier.cs @@ -163,7 +163,7 @@ public sealed class AutoDesignApplier : IDisposable { Reduce(data.Objects[0], state, newSet, _config.RespectManualOnAutomationUpdate, false, true, out var forcedRedraw); foreach (var actor in data.Objects) - _state.ReapplyState(actor, forcedRedraw, StateSource.Fixed); + _state.ReapplyAutomationState(actor, forcedRedraw, false, StateSource.Fixed); } } else if (_objects.TryGetValueAllWorld(id, out data) || _objects.TryGetValueNonOwned(id, out data)) @@ -174,7 +174,7 @@ public sealed class AutoDesignApplier : IDisposable if (_state.GetOrCreate(specificId, actor, out var state)) { Reduce(actor, state, newSet, _config.RespectManualOnAutomationUpdate, false, true, out var forcedRedraw); - _state.ReapplyState(actor, forcedRedraw, StateSource.Fixed); + _state.ReapplyAutomationState(actor, forcedRedraw, false, StateSource.Fixed); } } } diff --git a/Glamourer/Interop/InventoryService.cs b/Glamourer/Interop/InventoryService.cs index 33ba5cf..6c4a223 100644 --- a/Glamourer/Interop/InventoryService.cs +++ b/Glamourer/Interop/InventoryService.cs @@ -13,18 +13,6 @@ namespace Glamourer.Interop; public sealed unsafe class InventoryService : IDisposable, IRequiredService { - // Called by EquipGearset, but returns a pointer instead of an int. - // This is the internal function processed by all sources of Equipping a gearset, - // such as hotbar gearset application and command gearset application - public const string EquipGearsetInternal = "40 55 53 56 57 41 57 48 8D AC 24 ?? ?? ?? ?? 48 81 EC ?? ?? ?? ?? 48 8B 05 ?? ?? ?? ?? 48 33 C4 48 89 85 ?? ?? ?? ?? 4C 63 FA"; - private delegate nint ChangeGearsetInternalDelegate(RaptureGearsetModule* module, uint gearsetId, byte glamourPlateId); - - [Signature(EquipGearsetInternal, DetourName = nameof(EquipGearSetInternalDetour))] - private readonly Hook _equipGearsetInternalHook = null!; - - // The following above is currently pending for an accepted PR in FFXIVCLientStructs. - // Once accepted, remove everything above this comment and replace EquipGearset with EquipGearsetInternal. - private readonly MovedEquipment _movedItemsEvent; private readonly EquippedGearset _gearsetEvent; private readonly List<(EquipSlot, uint, StainIds)> _itemList = new(12); @@ -34,24 +22,29 @@ public sealed unsafe class InventoryService : IDisposable, IRequiredService _gearsetEvent = gearsetEvent; _moveItemHook = interop.HookFromAddress((nint)InventoryManager.MemberFunctionPointers.MoveItemSlot, MoveItemDetour); - _equipGearsetHook = interop.HookFromAddress((nint)RaptureGearsetModule.MemberFunctionPointers.EquipGearset, EquipGearSetDetour); + // This can be uncommented after ClientStructs Updates with EquipGearsetInternal after merged PR. (See comment below) + //_equipGearsetInternalHook = interop.HookFromAddress((nint)RaptureGearsetModule.MemberFunctionPointers.EquipGearsetInternal, EquipGearSetInternalDetour); + + // Can be removed after ClientStructs Update since this is only needed for current EquipGearsetInternal [Signature] interop.InitializeFromAttributes(this); _moveItemHook.Enable(); - _equipGearsetHook.Enable(); _equipGearsetInternalHook.Enable(); } public void Dispose() { _moveItemHook.Dispose(); - _equipGearsetHook.Dispose(); _equipGearsetInternalHook.Dispose(); } - private delegate int EquipGearsetDelegate(RaptureGearsetModule* module, int gearsetId, byte glamourPlateId); + // This is the internal function processed by all sources of Equipping a gearset, such as hotbar gearset application and command gearset application. + // Currently is pending to ClientStructs for integration. See: https://github.com/aers/FFXIVClientStructs/pull/1277 + public const string EquipGearsetInternal = "40 55 53 56 57 41 57 48 8D AC 24 ?? ?? ?? ?? 48 81 EC ?? ?? ?? ?? 48 8B 05 ?? ?? ?? ?? 48 33 C4 48 89 85 ?? ?? ?? ?? 4C 63 FA"; + private delegate nint EquipGearsetInternalDelegate(RaptureGearsetModule* module, uint gearsetId, byte glamourPlateId); - private readonly Hook _equipGearsetHook; + [Signature(EquipGearsetInternal, DetourName = nameof(EquipGearSetInternalDetour))] + private readonly Hook _equipGearsetInternalHook = null!; private nint EquipGearSetInternalDetour(RaptureGearsetModule* module, uint gearsetId, byte glamourPlateId) { @@ -132,14 +125,6 @@ public sealed unsafe class InventoryService : IDisposable, IRequiredService return ret; } - // Remove once internal is added. This no longer serves any purpose. - private int EquipGearSetDetour(RaptureGearsetModule* module, int gearsetId, byte glamourPlateId) - { - var ret = _equipGearsetHook.Original(module, gearsetId, glamourPlateId); - Glamourer.Log.Excessive($"[InventoryService] (old) Applied gear set {gearsetId} with glamour plate {glamourPlateId} (Returned {ret})"); - return ret; - } - private static uint FixId(uint itemId) => itemId % 50000; diff --git a/Glamourer/Interop/UpdateSlotService.cs b/Glamourer/Interop/UpdateSlotService.cs index 9ee8d8f..5f6e9cb 100644 --- a/Glamourer/Interop/UpdateSlotService.cs +++ b/Glamourer/Interop/UpdateSlotService.cs @@ -11,9 +11,9 @@ using Penumbra.GameData.Structs; namespace Glamourer.Interop; -// This struct is implemented into a PR for FFXIVClientStructs. Once merged, remove this struct and reference the data in ClientStructs instead. +// Can be removed once merged with client structs and referenced directly. See: https://github.com/aers/FFXIVClientStructs/pull/1277/files [StructLayout(LayoutKind.Explicit)] -public readonly struct GearsetItemDataStruct +public readonly struct GearsetDataStruct { // Stores the weapon data. Includes both dyes in the data. [FieldOffset(0)] public readonly WeaponModelId MainhandWeaponData; @@ -53,11 +53,6 @@ public readonly struct GearsetItemDataStruct public unsafe class UpdateSlotService : IDisposable { - // This function is what calls the weapon/equipment/crest loads, which call FlagSlotForUpdate if different. (MetaData not included) - public const string LoadGearsetDataSig = "48 89 5C 24 ?? 55 56 57 41 54 41 55 41 56 41 57 48 81 EC ?? ?? ?? ?? 48 8B 05 ?? ?? ?? ?? 48 33 C4 48 89 84 24 ?? ?? ?? ?? 44 0F B6 B9"; - private delegate Int64 LoadGearsetDataDelegate(DrawDataContainer* drawDataContainer, GearsetItemDataStruct* gearsetData); - // The above can be removed after the FFXIVClientStruct Merge is made! - public readonly EquipSlotUpdating EquipSlotUpdatingEvent; public readonly BonusSlotUpdating BonusSlotUpdatingEvent; public readonly GearsetDataLoaded GearsetDataLoadedEvent; @@ -68,9 +63,12 @@ public unsafe class UpdateSlotService : IDisposable EquipSlotUpdatingEvent = equipSlotUpdating; BonusSlotUpdatingEvent = bonusSlotUpdating; GearsetDataLoadedEvent = gearsetDataLoaded; - _bonusItems = bonusItems; + + // Usable after the merge with client structs. + //_loadGearsetDataHook = interop.HookFromAddress((nint)DrawDataContainer.MemberFunctionPointers.LoadGearsetData, LoadGearsetDataDetour); interop.InitializeFromAttributes(this); + _flagSlotForUpdateHook.Enable(); _flagBonusSlotForUpdateHook.Enable(); _loadGearsetDataHook.Enable(); @@ -129,6 +127,19 @@ public unsafe class UpdateSlotService : IDisposable [Signature(Sigs.FlagBonusSlotForUpdate, DetourName = nameof(FlagBonusSlotForUpdateDetour))] private readonly Hook _flagBonusSlotForUpdateHook = null!; + // This signature is what calls the weapon/equipment/crest load functions in the drawData container inherited from a human/characterBase. + // + // Contrary to assumption, this is not frequently fired when any slot changes, and is instead only called when another player + // initially loads, or when the client player changes gearsets. (Does not fire when another player or self is redrawn) + // + // This functions purpose is to iterate all Equipment/Weapon/Crest data on gearset change / initial player load, and determine which slots need to fire FlagSlotForUpdate. + // + // Because Glamourer processes GameState changes by detouring this method, this means by returning original after detour, any logic performed after will occur + // AFTER Glamourer finishes applying all changes to the game State, providing a gearset endpoint. (MetaData not included) + // Currently pending a merge to clientStructs, after which it can be removed, along with the explicit struct. See: https://github.com/aers/FFXIVClientStructs/pull/1277/files + public const string LoadGearsetDataSig = "48 89 5C 24 ?? 55 56 57 41 54 41 55 41 56 41 57 48 81 EC ?? ?? ?? ?? 48 8B 05 ?? ?? ?? ?? 48 33 C4 48 89 84 24 ?? ?? ?? ?? 44 0F B6 B9"; + private delegate Int64 LoadGearsetDataDelegate(DrawDataContainer* drawDataContainer, GearsetDataStruct* gearsetData); + [Signature(LoadGearsetDataSig, DetourName = nameof(LoadGearsetDataDetour))] private readonly Hook _loadGearsetDataHook = null!; @@ -157,23 +168,20 @@ public unsafe class UpdateSlotService : IDisposable Glamourer.Log.Excessive($"[FlagBonusSlotForUpdate] Invoked by Glamourer on 0x{drawObject.Address:X} on {slot} with itemdata {armor}."); return _flagSlotForUpdateHook.Original(drawObject.Address, slot.ToIndex(), &armor); } - private Int64 LoadGearsetDataDetour(DrawDataContainer* drawDataContainer, GearsetItemDataStruct* gearsetData) + private Int64 LoadGearsetDataDetour(DrawDataContainer* drawDataContainer, GearsetDataStruct* gearsetData) { // Let the gearset data process all of its loads and slot flag update calls first. var ret = _loadGearsetDataHook.Original(drawDataContainer, gearsetData); - Model ownerDrawObject = drawDataContainer->OwnerObject->DrawObject; - if (!ownerDrawObject.IsCharacterBase) - return ret; - - // invoke the changed event for the state listener and return. - Glamourer.Log.Verbose($"[LoadAllEquipmentDetour] Owner: 0x{ownerDrawObject.Address:X} Finished Applying its GameState!"); + Model drawObject = drawDataContainer->OwnerObject->DrawObject; + Glamourer.Log.Verbose($"[LoadAllEquipmentDetour] Owner: 0x{drawObject.Address:X} Finished Applying its GameState!"); + GearsetDataLoadedEvent.Invoke(drawObject); + // Can use for debugging, if desired. // Glamourer.Log.Verbose($"[LoadAllEquipmentDetour] GearsetItemData: {FormatGearsetItemDataStruct(*gearsetData)}"); - GearsetDataLoadedEvent.Invoke(drawDataContainer->OwnerObject->DrawObject); return ret; } - // If you ever care to debug this, here is a formatted string output of this new gearsetDataPacket struct. - private string FormatGearsetItemDataStruct(GearsetItemDataStruct gearsetData) + // If you ever care to debug this, here is a formatted string output of this new gearsetData struct. + private string FormatGearsetItemDataStruct(GearsetDataStruct gearsetData) { string ret = $"\nMainhandWeaponData: Id: {gearsetData.MainhandWeaponData.Id}, Type: {gearsetData.MainhandWeaponData.Type}, " + From 1cd8e5fb7ec29097aaa2e413fce01a8aadb7a0cf Mon Sep 17 00:00:00 2001 From: Cordelia Mist Date: Sun, 19 Jan 2025 09:35:10 -0800 Subject: [PATCH 06/10] Corrected comments on StateApi for PR --- Glamourer/Api/StateApi.cs | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/Glamourer/Api/StateApi.cs b/Glamourer/Api/StateApi.cs index 34f4ad9..cb7fe51 100644 --- a/Glamourer/Api/StateApi.cs +++ b/Glamourer/Api/StateApi.cs @@ -340,9 +340,7 @@ public sealed class StateApi : IGlamourerApiState, IApiService, IDisposable private void OnStateChanged(StateChangeType type, StateSource _2, ActorState _3, ActorData actors, ITransaction? _5) { - // Remove this comment before creating PR. - Glamourer.Log.Verbose($"[OnStateChanged] Sending out OnStateChanged with type {type}."); - + // Glamourer.Log.Verbose($"[OnStateChanged] Sending out OnStateChanged with type {type}."); if (StateChanged != null) foreach (var actor in actors.Objects) StateChanged.Invoke(actor.Address); @@ -354,13 +352,9 @@ public sealed class StateApi : IGlamourerApiState, IApiService, IDisposable private void OnStateUpdated(StateUpdateType type, ActorData actors) { + // Glamourer.Log.Verbose($"[OnStateUpdated] Sending out OnStateUpdated with type {type}."); if (StateUpdated != null) foreach (var actor in actors.Objects) - { - // Remove these before creating PR - Glamourer.Log.Information($"[ENDPOINT DEBUGGING] 0x{actor.Address:X} had update of type {type}."); - Glamourer.Log.Information("--------------------------------------------------------------"); StateUpdated.Invoke(actor.Address, type); - } } } From ebdfd3f8bc56dcd7a12f4cb3e9f84164e8dd946e Mon Sep 17 00:00:00 2001 From: Cordelia Mist Date: Sun, 19 Jan 2025 09:50:34 -0800 Subject: [PATCH 07/10] Correct spacing and formatting further to align with main Glamourer branch preferences. --- Glamourer/Interop/InventoryService.cs | 71 ++++++++++---------- Glamourer/Interop/UpdateSlotService.cs | 91 +++++++++++++------------- Glamourer/State/StateEditor.cs | 5 +- 3 files changed, 83 insertions(+), 84 deletions(-) diff --git a/Glamourer/Interop/InventoryService.cs b/Glamourer/Interop/InventoryService.cs index 6c4a223..f0ed6b5 100644 --- a/Glamourer/Interop/InventoryService.cs +++ b/Glamourer/Interop/InventoryService.cs @@ -13,9 +13,10 @@ namespace Glamourer.Interop; public sealed unsafe class InventoryService : IDisposable, IRequiredService { - private readonly MovedEquipment _movedItemsEvent; - private readonly EquippedGearset _gearsetEvent; + private readonly MovedEquipment _movedItemsEvent; + private readonly EquippedGearset _gearsetEvent; private readonly List<(EquipSlot, uint, StainIds)> _itemList = new(12); + public InventoryService(MovedEquipment movedItemsEvent, IGameInteropProvider interop, EquippedGearset gearsetEvent) { _movedItemsEvent = movedItemsEvent; @@ -80,18 +81,18 @@ public sealed unsafe class InventoryService : IDisposable, IRequiredService } var plate = MirageManager.Instance()->GlamourPlates[glamourPlateId - 1]; - Add(EquipSlot.MainHand, plate.ItemIds[0], StainIds.FromGlamourPlate(plate, 0), ref entry->Items[0]); - Add(EquipSlot.OffHand, plate.ItemIds[1], StainIds.FromGlamourPlate(plate, 1), ref entry->Items[1]); - Add(EquipSlot.Head, plate.ItemIds[2], StainIds.FromGlamourPlate(plate, 2), ref entry->Items[2]); - Add(EquipSlot.Body, plate.ItemIds[3], StainIds.FromGlamourPlate(plate, 3), ref entry->Items[3]); - Add(EquipSlot.Hands, plate.ItemIds[4], StainIds.FromGlamourPlate(plate, 4), ref entry->Items[5]); - Add(EquipSlot.Legs, plate.ItemIds[5], StainIds.FromGlamourPlate(plate, 5), ref entry->Items[6]); - Add(EquipSlot.Feet, plate.ItemIds[6], StainIds.FromGlamourPlate(plate, 6), ref entry->Items[7]); - Add(EquipSlot.Ears, plate.ItemIds[7], StainIds.FromGlamourPlate(plate, 7), ref entry->Items[8]); - Add(EquipSlot.Neck, plate.ItemIds[8], StainIds.FromGlamourPlate(plate, 8), ref entry->Items[9]); - Add(EquipSlot.Wrists, plate.ItemIds[9], StainIds.FromGlamourPlate(plate, 9), ref entry->Items[10]); - Add(EquipSlot.RFinger, plate.ItemIds[10], StainIds.FromGlamourPlate(plate, 10), ref entry->Items[11]); - Add(EquipSlot.LFinger, plate.ItemIds[11], StainIds.FromGlamourPlate(plate, 11), ref entry->Items[12]); + Add(EquipSlot.MainHand, plate.ItemIds[0], StainIds.FromGlamourPlate(plate, 0), ref entry->Items[0]); + Add(EquipSlot.OffHand, plate.ItemIds[1], StainIds.FromGlamourPlate(plate, 1), ref entry->Items[1]); + Add(EquipSlot.Head, plate.ItemIds[2], StainIds.FromGlamourPlate(plate, 2), ref entry->Items[2]); + Add(EquipSlot.Body, plate.ItemIds[3], StainIds.FromGlamourPlate(plate, 3), ref entry->Items[3]); + Add(EquipSlot.Hands, plate.ItemIds[4], StainIds.FromGlamourPlate(plate, 4), ref entry->Items[5]); + Add(EquipSlot.Legs, plate.ItemIds[5], StainIds.FromGlamourPlate(plate, 5), ref entry->Items[6]); + Add(EquipSlot.Feet, plate.ItemIds[6], StainIds.FromGlamourPlate(plate, 6), ref entry->Items[7]); + Add(EquipSlot.Ears, plate.ItemIds[7], StainIds.FromGlamourPlate(plate, 7), ref entry->Items[8]); + Add(EquipSlot.Neck, plate.ItemIds[8], StainIds.FromGlamourPlate(plate, 8), ref entry->Items[9]); + Add(EquipSlot.Wrists, plate.ItemIds[9], StainIds.FromGlamourPlate(plate, 9), ref entry->Items[10]); + Add(EquipSlot.RFinger, plate.ItemIds[10], StainIds.FromGlamourPlate(plate, 10), ref entry->Items[11]); + Add(EquipSlot.LFinger, plate.ItemIds[11], StainIds.FromGlamourPlate(plate, 11), ref entry->Items[12]); } else { @@ -106,17 +107,17 @@ public sealed unsafe class InventoryService : IDisposable, IRequiredService } Add(EquipSlot.MainHand, ref entry->Items[0]); - Add(EquipSlot.OffHand, ref entry->Items[1]); - Add(EquipSlot.Head, ref entry->Items[2]); - Add(EquipSlot.Body, ref entry->Items[3]); - Add(EquipSlot.Hands, ref entry->Items[5]); - Add(EquipSlot.Legs, ref entry->Items[6]); - Add(EquipSlot.Feet, ref entry->Items[7]); - Add(EquipSlot.Ears, ref entry->Items[8]); - Add(EquipSlot.Neck, ref entry->Items[9]); - Add(EquipSlot.Wrists, ref entry->Items[10]); - Add(EquipSlot.RFinger, ref entry->Items[11]); - Add(EquipSlot.LFinger, ref entry->Items[12]); + Add(EquipSlot.OffHand, ref entry->Items[1]); + Add(EquipSlot.Head, ref entry->Items[2]); + Add(EquipSlot.Body, ref entry->Items[3]); + Add(EquipSlot.Hands, ref entry->Items[5]); + Add(EquipSlot.Legs, ref entry->Items[6]); + Add(EquipSlot.Feet, ref entry->Items[7]); + Add(EquipSlot.Ears, ref entry->Items[8]); + Add(EquipSlot.Neck, ref entry->Items[9]); + Add(EquipSlot.Wrists, ref entry->Items[10]); + Add(EquipSlot.RFinger, ref entry->Items[11]); + Add(EquipSlot.LFinger, ref entry->Items[12]); } _movedItemsEvent.Invoke(_itemList.ToArray()); @@ -203,18 +204,18 @@ public sealed unsafe class InventoryService : IDisposable, IRequiredService private static EquipSlot GetSlot(uint slot) => slot switch { - 0 => EquipSlot.MainHand, - 1 => EquipSlot.OffHand, - 2 => EquipSlot.Head, - 3 => EquipSlot.Body, - 4 => EquipSlot.Hands, - 6 => EquipSlot.Legs, - 7 => EquipSlot.Feet, - 8 => EquipSlot.Ears, - 9 => EquipSlot.Neck, + 0 => EquipSlot.MainHand, + 1 => EquipSlot.OffHand, + 2 => EquipSlot.Head, + 3 => EquipSlot.Body, + 4 => EquipSlot.Hands, + 6 => EquipSlot.Legs, + 7 => EquipSlot.Feet, + 8 => EquipSlot.Ears, + 9 => EquipSlot.Neck, 10 => EquipSlot.Wrists, 11 => EquipSlot.RFinger, 12 => EquipSlot.LFinger, - _ => EquipSlot.Unknown, + _ => EquipSlot.Unknown, }; } diff --git a/Glamourer/Interop/UpdateSlotService.cs b/Glamourer/Interop/UpdateSlotService.cs index 5f6e9cb..e453c6e 100644 --- a/Glamourer/Interop/UpdateSlotService.cs +++ b/Glamourer/Interop/UpdateSlotService.cs @@ -11,46 +11,6 @@ using Penumbra.GameData.Structs; namespace Glamourer.Interop; -// Can be removed once merged with client structs and referenced directly. See: https://github.com/aers/FFXIVClientStructs/pull/1277/files -[StructLayout(LayoutKind.Explicit)] -public readonly struct GearsetDataStruct -{ - // Stores the weapon data. Includes both dyes in the data. - [FieldOffset(0)] public readonly WeaponModelId MainhandWeaponData; - [FieldOffset(8)] public readonly WeaponModelId OffhandWeaponData; - - [FieldOffset(16)] public readonly byte CrestBitField; // A Bitfield:: ShieldCrest == 1, HeadCrest == 2, Chest Crest == 4 - [FieldOffset(17)] public readonly byte JobId; // Job ID associated with the gearset change. - - // Flicks from 0 to 127 (anywhere inbetween), have yet to associate what it is linked to. Remains the same when flicking between gearsets of the same job. - [FieldOffset(18)] public readonly byte UNK_18; - [FieldOffset(19)] public readonly byte UNK_19; // I have never seen this be anything other than 0. - - // Legacy helmet equip slot armor for a character. - [FieldOffset(20)] public readonly LegacyCharacterArmor HeadSlotArmor; - [FieldOffset(24)] public readonly LegacyCharacterArmor TopSlotArmor; - [FieldOffset(28)] public readonly LegacyCharacterArmor ArmsSlotArmor; - [FieldOffset(32)] public readonly LegacyCharacterArmor LegsSlotArmor; - [FieldOffset(26)] public readonly LegacyCharacterArmor FeetSlotArmor; - [FieldOffset(40)] public readonly LegacyCharacterArmor EarSlotArmor; - [FieldOffset(44)] public readonly LegacyCharacterArmor NeckSlotArmor; - [FieldOffset(48)] public readonly LegacyCharacterArmor WristSlotArmor; - [FieldOffset(52)] public readonly LegacyCharacterArmor RFingerSlotArmor; - [FieldOffset(56)] public readonly LegacyCharacterArmor LFingerSlotArmor; - - // Byte array of all slot's secondary dyes. - [FieldOffset(60)] public readonly byte HeadSlotSecondaryDye; - [FieldOffset(61)] public readonly byte TopSlotSecondaryDye; - [FieldOffset(62)] public readonly byte ArmsSlotSecondaryDye; - [FieldOffset(63)] public readonly byte LegsSlotSecondaryDye; - [FieldOffset(64)] public readonly byte FeetSlotSecondaryDye; - [FieldOffset(65)] public readonly byte EarSlotSecondaryDye; - [FieldOffset(66)] public readonly byte NeckSlotSecondaryDye; - [FieldOffset(67)] public readonly byte WristSlotSecondaryDye; - [FieldOffset(68)] public readonly byte RFingerSlotSecondaryDye; - [FieldOffset(69)] public readonly byte LFingerSlotSecondaryDye; -} - public unsafe class UpdateSlotService : IDisposable { public readonly EquipSlotUpdating EquipSlotUpdatingEvent; @@ -145,22 +105,20 @@ public unsafe class UpdateSlotService : IDisposable private ulong FlagSlotForUpdateDetour(nint drawObject, uint slotIdx, CharacterArmor* data) { - var slot = slotIdx.ToEquipSlot(); + var slot = slotIdx.ToEquipSlot(); var returnValue = ulong.MaxValue; EquipSlotUpdatingEvent.Invoke(drawObject, slot, ref *data, ref returnValue); Glamourer.Log.Excessive($"[FlagSlotForUpdate] Called with 0x{drawObject:X} for slot {slot} with {*data} ({returnValue:X})."); - returnValue = returnValue == ulong.MaxValue ? _flagSlotForUpdateHook.Original(drawObject, slotIdx, data) : returnValue; - return returnValue; + return returnValue == ulong.MaxValue ? _flagSlotForUpdateHook.Original(drawObject, slotIdx, data) : returnValue; } private ulong FlagBonusSlotForUpdateDetour(nint drawObject, uint slotIdx, CharacterArmor* data) { - var slot = slotIdx.ToBonusSlot(); + var slot = slotIdx.ToBonusSlot(); var returnValue = ulong.MaxValue; BonusSlotUpdatingEvent.Invoke(drawObject, slot, ref *data, ref returnValue); Glamourer.Log.Excessive($"[FlagBonusSlotForUpdate] Called with 0x{drawObject:X} for slot {slot} with {*data} ({returnValue:X})."); - returnValue = returnValue == ulong.MaxValue ? _flagBonusSlotForUpdateHook.Original(drawObject, slotIdx, data) : returnValue; - return returnValue; + return returnValue == ulong.MaxValue ? _flagBonusSlotForUpdateHook.Original(drawObject, slotIdx, data) : returnValue; } private ulong FlagSlotForUpdateInterop(Model drawObject, EquipSlot slot, CharacterArmor armor) @@ -175,7 +133,6 @@ public unsafe class UpdateSlotService : IDisposable Model drawObject = drawDataContainer->OwnerObject->DrawObject; Glamourer.Log.Verbose($"[LoadAllEquipmentDetour] Owner: 0x{drawObject.Address:X} Finished Applying its GameState!"); GearsetDataLoadedEvent.Invoke(drawObject); - // Can use for debugging, if desired. // Glamourer.Log.Verbose($"[LoadAllEquipmentDetour] GearsetItemData: {FormatGearsetItemDataStruct(*gearsetData)}"); return ret; } @@ -199,3 +156,43 @@ public unsafe class UpdateSlotService : IDisposable return ret; } } + +// Can be removed once merged with client structs and referenced directly. See: https://github.com/aers/FFXIVClientStructs/pull/1277/files +[StructLayout(LayoutKind.Explicit)] +public readonly struct GearsetDataStruct +{ + // Stores the weapon data. Includes both dyes in the data. + [FieldOffset(0)] public readonly WeaponModelId MainhandWeaponData; + [FieldOffset(8)] public readonly WeaponModelId OffhandWeaponData; + + [FieldOffset(16)] public readonly byte CrestBitField; // A Bitfield:: ShieldCrest == 1, HeadCrest == 2, Chest Crest == 4 + [FieldOffset(17)] public readonly byte JobId; // Job ID associated with the gearset change. + + // Flicks from 0 to 127 (anywhere inbetween), have yet to associate what it is linked to. Remains the same when flicking between gearsets of the same job. + [FieldOffset(18)] public readonly byte UNK_18; + [FieldOffset(19)] public readonly byte UNK_19; // I have never seen this be anything other than 0. + + // Legacy helmet equip slot armor for a character. + [FieldOffset(20)] public readonly LegacyCharacterArmor HeadSlotArmor; + [FieldOffset(24)] public readonly LegacyCharacterArmor TopSlotArmor; + [FieldOffset(28)] public readonly LegacyCharacterArmor ArmsSlotArmor; + [FieldOffset(32)] public readonly LegacyCharacterArmor LegsSlotArmor; + [FieldOffset(26)] public readonly LegacyCharacterArmor FeetSlotArmor; + [FieldOffset(40)] public readonly LegacyCharacterArmor EarSlotArmor; + [FieldOffset(44)] public readonly LegacyCharacterArmor NeckSlotArmor; + [FieldOffset(48)] public readonly LegacyCharacterArmor WristSlotArmor; + [FieldOffset(52)] public readonly LegacyCharacterArmor RFingerSlotArmor; + [FieldOffset(56)] public readonly LegacyCharacterArmor LFingerSlotArmor; + + // Byte array of all slot's secondary dyes. + [FieldOffset(60)] public readonly byte HeadSlotSecondaryDye; + [FieldOffset(61)] public readonly byte TopSlotSecondaryDye; + [FieldOffset(62)] public readonly byte ArmsSlotSecondaryDye; + [FieldOffset(63)] public readonly byte LegsSlotSecondaryDye; + [FieldOffset(64)] public readonly byte FeetSlotSecondaryDye; + [FieldOffset(65)] public readonly byte EarSlotSecondaryDye; + [FieldOffset(66)] public readonly byte NeckSlotSecondaryDye; + [FieldOffset(67)] public readonly byte WristSlotSecondaryDye; + [FieldOffset(68)] public readonly byte RFingerSlotSecondaryDye; + [FieldOffset(69)] public readonly byte LFingerSlotSecondaryDye; +} diff --git a/Glamourer/State/StateEditor.cs b/Glamourer/State/StateEditor.cs index 891c61d..b122352 100644 --- a/Glamourer/State/StateEditor.cs +++ b/Glamourer/State/StateEditor.cs @@ -253,7 +253,7 @@ public class StateEditor( return; var actors = Applier.ChangeMetaState(state, index, settings.Source.RequiresChange()); - Glamourer.Log.Debug( + Glamourer.Log.Verbose( $"Set {index.ToName()} in state {state.Identifier.Incognito(null)} from {old} to {value}. [Affecting {actors.ToLazyString("nothing")}.]"); StateChanged.Invoke(StateChangeType.Other, settings.Source, state, actors, new MetaTransaction(index, old, value)); } @@ -417,7 +417,8 @@ public class StateEditor( ? Applier.ApplyAll(state, requiresRedraw, false) : ActorData.Invalid; - Glamourer.Log.Verbose($"Applied design to {state.Identifier.Incognito(null)}. [Affecting {actors.ToLazyString("nothing")}.]"); + Glamourer.Log.Verbose( + $"Applied design to {state.Identifier.Incognito(null)}. [Affecting {actors.ToLazyString("nothing")}.]"); StateChanged.Invoke(StateChangeType.Design, state.Sources[MetaIndex.Wetness], state, actors, null); // FIXME: maybe later if(settings.SendStateUpdate) StateUpdated.Invoke(StateUpdateType.DesignApplied, actors); From c43ce9d978f57f10f62f8c981533a5344cd4b638 Mon Sep 17 00:00:00 2001 From: Cordelia Mist Date: Tue, 21 Jan 2025 10:47:12 -0800 Subject: [PATCH 08/10] Updated new functionality and internal gearset to use FFXIVClientStructs member functions and implemented structs, corrected remaining spacing issues. --- Glamourer/Api/StateApi.cs | 5 +- Glamourer/Events/GearsetDataLoaded.cs | 8 +-- Glamourer/Interop/InventoryService.cs | 20 ++---- Glamourer/Interop/UpdateSlotService.cs | 89 ++++++-------------------- Glamourer/State/StateEditor.cs | 2 +- Glamourer/State/StateListener.cs | 3 +- 6 files changed, 33 insertions(+), 94 deletions(-) diff --git a/Glamourer/Api/StateApi.cs b/Glamourer/Api/StateApi.cs index cb7fe51..41f7650 100644 --- a/Glamourer/Api/StateApi.cs +++ b/Glamourer/Api/StateApi.cs @@ -14,7 +14,6 @@ using StateChanged = Glamourer.Events.StateChanged; using StateUpdated = Glamourer.Events.StateUpdated; namespace Glamourer.Api; - public sealed class StateApi : IGlamourerApiState, IApiService, IDisposable { private readonly ApiHelpers _helpers; @@ -340,7 +339,7 @@ public sealed class StateApi : IGlamourerApiState, IApiService, IDisposable private void OnStateChanged(StateChangeType type, StateSource _2, ActorState _3, ActorData actors, ITransaction? _5) { - // Glamourer.Log.Verbose($"[OnStateChanged] Sending out OnStateChanged with type {type}."); + Glamourer.Log.Excessive($"[OnStateChanged] State Changed with Type {type} [Affecting {actors.ToLazyString("nothing")}.]"); if (StateChanged != null) foreach (var actor in actors.Objects) StateChanged.Invoke(actor.Address); @@ -352,7 +351,7 @@ public sealed class StateApi : IGlamourerApiState, IApiService, IDisposable private void OnStateUpdated(StateUpdateType type, ActorData actors) { - // Glamourer.Log.Verbose($"[OnStateUpdated] Sending out OnStateUpdated with type {type}."); + Glamourer.Log.Verbose($"[OnStateUpdated] State Updated with Type {type}. [Affecting {actors.ToLazyString("nothing")}.]"); if (StateUpdated != null) foreach (var actor in actors.Objects) StateUpdated.Invoke(actor.Address, type); diff --git a/Glamourer/Events/GearsetDataLoaded.cs b/Glamourer/Events/GearsetDataLoaded.cs index dd12bc1..4750939 100644 --- a/Glamourer/Events/GearsetDataLoaded.cs +++ b/Glamourer/Events/GearsetDataLoaded.cs @@ -4,10 +4,10 @@ using Penumbra.GameData.Interop; namespace Glamourer.Events; /// -/// Triggers when the equipped gearset finished running all of its LoadEquipment, LoadWeapon, and crest calls. -/// This defines a universal endpoint of base game state application to monitor. +/// Triggers when the equipped gearset finished all LoadEquipment, LoadWeapon, and LoadCrest calls. (All Non-MetaData) +/// This defines an endpoint for when the gameState is updated. /// -/// The model drawobject associated with the finished load (Also fired by other players on render) +/// The model draw object associated with the finished load (Also fired by other players on render) /// /// public sealed class GearsetDataLoaded() @@ -18,4 +18,4 @@ public sealed class GearsetDataLoaded() /// StateListener = 0, } -} \ No newline at end of file +} diff --git a/Glamourer/Interop/InventoryService.cs b/Glamourer/Interop/InventoryService.cs index f0ed6b5..4b98d46 100644 --- a/Glamourer/Interop/InventoryService.cs +++ b/Glamourer/Interop/InventoryService.cs @@ -23,34 +23,26 @@ public sealed unsafe class InventoryService : IDisposable, IRequiredService _gearsetEvent = gearsetEvent; _moveItemHook = interop.HookFromAddress((nint)InventoryManager.MemberFunctionPointers.MoveItemSlot, MoveItemDetour); - // This can be uncommented after ClientStructs Updates with EquipGearsetInternal after merged PR. (See comment below) - //_equipGearsetInternalHook = interop.HookFromAddress((nint)RaptureGearsetModule.MemberFunctionPointers.EquipGearsetInternal, EquipGearSetInternalDetour); - - // Can be removed after ClientStructs Update since this is only needed for current EquipGearsetInternal [Signature] - interop.InitializeFromAttributes(this); + _equipGearsetHook = interop.HookFromAddress((nint)RaptureGearsetModule.MemberFunctionPointers.EquipGearsetInternal, EquipGearSetDetour); _moveItemHook.Enable(); - _equipGearsetInternalHook.Enable(); + _equipGearsetHook.Enable(); } public void Dispose() { _moveItemHook.Dispose(); - _equipGearsetInternalHook.Dispose(); + _equipGearsetHook.Dispose(); } - // This is the internal function processed by all sources of Equipping a gearset, such as hotbar gearset application and command gearset application. - // Currently is pending to ClientStructs for integration. See: https://github.com/aers/FFXIVClientStructs/pull/1277 - public const string EquipGearsetInternal = "40 55 53 56 57 41 57 48 8D AC 24 ?? ?? ?? ?? 48 81 EC ?? ?? ?? ?? 48 8B 05 ?? ?? ?? ?? 48 33 C4 48 89 85 ?? ?? ?? ?? 4C 63 FA"; private delegate nint EquipGearsetInternalDelegate(RaptureGearsetModule* module, uint gearsetId, byte glamourPlateId); - [Signature(EquipGearsetInternal, DetourName = nameof(EquipGearSetInternalDetour))] - private readonly Hook _equipGearsetInternalHook = null!; + private readonly Hook _equipGearsetHook = null!; - private nint EquipGearSetInternalDetour(RaptureGearsetModule* module, uint gearsetId, byte glamourPlateId) + private nint EquipGearSetDetour(RaptureGearsetModule* module, uint gearsetId, byte glamourPlateId) { var prior = module->CurrentGearsetIndex; - var ret = _equipGearsetInternalHook.Original(module, gearsetId, glamourPlateId); + var ret = _equipGearsetHook.Original(module, gearsetId, glamourPlateId); var set = module->GetGearset((int)gearsetId); _gearsetEvent.Invoke(new ByteString(set->Name).ToString(), (int)gearsetId, prior, glamourPlateId, set->ClassJob); Glamourer.Log.Verbose($"[InventoryService] Applied gear set {gearsetId} with glamour plate {glamourPlateId} (Returned {ret})"); diff --git a/Glamourer/Interop/UpdateSlotService.cs b/Glamourer/Interop/UpdateSlotService.cs index e453c6e..466f1ae 100644 --- a/Glamourer/Interop/UpdateSlotService.cs +++ b/Glamourer/Interop/UpdateSlotService.cs @@ -2,6 +2,7 @@ using Dalamud.Plugin.Services; using Dalamud.Utility.Signatures; using FFXIVClientStructs.FFXIV.Client.Game.Character; +using FFXIVClientStructs.FFXIV.Client.Game.Network; using Glamourer.Events; using Penumbra.GameData; using Penumbra.GameData.DataContainers; @@ -13,22 +14,20 @@ namespace Glamourer.Interop; public unsafe class UpdateSlotService : IDisposable { - public readonly EquipSlotUpdating EquipSlotUpdatingEvent; - public readonly BonusSlotUpdating BonusSlotUpdatingEvent; - public readonly GearsetDataLoaded GearsetDataLoadedEvent; - private readonly DictBonusItems _bonusItems; + public readonly EquipSlotUpdating EquipSlotUpdatingEvent; + public readonly BonusSlotUpdating BonusSlotUpdatingEvent; + public readonly GearsetDataLoaded GearsetDataLoadedEvent; + private readonly DictBonusItems _bonusItems; public UpdateSlotService(EquipSlotUpdating equipSlotUpdating, BonusSlotUpdating bonusSlotUpdating, GearsetDataLoaded gearsetDataLoaded, IGameInteropProvider interop, DictBonusItems bonusItems) { EquipSlotUpdatingEvent = equipSlotUpdating; BonusSlotUpdatingEvent = bonusSlotUpdating; GearsetDataLoadedEvent = gearsetDataLoaded; - _bonusItems = bonusItems; + _bonusItems = bonusItems; - // Usable after the merge with client structs. - //_loadGearsetDataHook = interop.HookFromAddress((nint)DrawDataContainer.MemberFunctionPointers.LoadGearsetData, LoadGearsetDataDetour); + _loadGearsetDataHook = interop.HookFromAddress((nint)DrawDataContainer.MemberFunctionPointers.LoadGearsetData, LoadGearsetDataDetour); interop.InitializeFromAttributes(this); - _flagSlotForUpdateHook.Enable(); _flagBonusSlotForUpdateHook.Enable(); _loadGearsetDataHook.Enable(); @@ -87,25 +86,15 @@ public unsafe class UpdateSlotService : IDisposable [Signature(Sigs.FlagBonusSlotForUpdate, DetourName = nameof(FlagBonusSlotForUpdateDetour))] private readonly Hook _flagBonusSlotForUpdateHook = null!; - // This signature is what calls the weapon/equipment/crest load functions in the drawData container inherited from a human/characterBase. - // - // Contrary to assumption, this is not frequently fired when any slot changes, and is instead only called when another player - // initially loads, or when the client player changes gearsets. (Does not fire when another player or self is redrawn) - // - // This functions purpose is to iterate all Equipment/Weapon/Crest data on gearset change / initial player load, and determine which slots need to fire FlagSlotForUpdate. - // - // Because Glamourer processes GameState changes by detouring this method, this means by returning original after detour, any logic performed after will occur - // AFTER Glamourer finishes applying all changes to the game State, providing a gearset endpoint. (MetaData not included) - // Currently pending a merge to clientStructs, after which it can be removed, along with the explicit struct. See: https://github.com/aers/FFXIVClientStructs/pull/1277/files - public const string LoadGearsetDataSig = "48 89 5C 24 ?? 55 56 57 41 54 41 55 41 56 41 57 48 81 EC ?? ?? ?? ?? 48 8B 05 ?? ?? ?? ?? 48 33 C4 48 89 84 24 ?? ?? ?? ?? 44 0F B6 B9"; - private delegate Int64 LoadGearsetDataDelegate(DrawDataContainer* drawDataContainer, GearsetDataStruct* gearsetData); - - [Signature(LoadGearsetDataSig, DetourName = nameof(LoadGearsetDataDetour))] + /// Detours the func that makes all FlagSlotForUpdate calls on a gearset change or initial render of a given actor (Only Cases this is Called). + /// Logic done after returning the original hook executes After all equipment/weapon/crest data is loaded into the Actors BaseData. + /// + private delegate Int64 LoadGearsetDataDelegate(DrawDataContainer* drawDataContainer, PacketPlayerGearsetData* gearsetData); private readonly Hook _loadGearsetDataHook = null!; private ulong FlagSlotForUpdateDetour(nint drawObject, uint slotIdx, CharacterArmor* data) { - var slot = slotIdx.ToEquipSlot(); + var slot = slotIdx.ToEquipSlot(); var returnValue = ulong.MaxValue; EquipSlotUpdatingEvent.Invoke(drawObject, slot, ref *data, ref returnValue); Glamourer.Log.Excessive($"[FlagSlotForUpdate] Called with 0x{drawObject:X} for slot {slot} with {*data} ({returnValue:X})."); @@ -114,7 +103,7 @@ public unsafe class UpdateSlotService : IDisposable private ulong FlagBonusSlotForUpdateDetour(nint drawObject, uint slotIdx, CharacterArmor* data) { - var slot = slotIdx.ToBonusSlot(); + var slot = slotIdx.ToBonusSlot(); var returnValue = ulong.MaxValue; BonusSlotUpdatingEvent.Invoke(drawObject, slot, ref *data, ref returnValue); Glamourer.Log.Excessive($"[FlagBonusSlotForUpdate] Called with 0x{drawObject:X} for slot {slot} with {*data} ({returnValue:X})."); @@ -123,29 +112,27 @@ public unsafe class UpdateSlotService : IDisposable private ulong FlagSlotForUpdateInterop(Model drawObject, EquipSlot slot, CharacterArmor armor) { - Glamourer.Log.Excessive($"[FlagBonusSlotForUpdate] Invoked by Glamourer on 0x{drawObject.Address:X} on {slot} with itemdata {armor}."); + Glamourer.Log.Excessive($"[FlagBonusSlotForUpdate] Glamourer-Invoked on 0x{drawObject.Address:X} on {slot} with item data {armor}."); return _flagSlotForUpdateHook.Original(drawObject.Address, slot.ToIndex(), &armor); } - private Int64 LoadGearsetDataDetour(DrawDataContainer* drawDataContainer, GearsetDataStruct* gearsetData) + private Int64 LoadGearsetDataDetour(DrawDataContainer* drawDataContainer, PacketPlayerGearsetData* gearsetData) { - // Let the gearset data process all of its loads and slot flag update calls first. var ret = _loadGearsetDataHook.Original(drawDataContainer, gearsetData); Model drawObject = drawDataContainer->OwnerObject->DrawObject; - Glamourer.Log.Verbose($"[LoadAllEquipmentDetour] Owner: 0x{drawObject.Address:X} Finished Applying its GameState!"); GearsetDataLoadedEvent.Invoke(drawObject); - // Glamourer.Log.Verbose($"[LoadAllEquipmentDetour] GearsetItemData: {FormatGearsetItemDataStruct(*gearsetData)}"); + // Glamourer.Log.Excessive($"[LoadAllEquipmentDetour] GearsetItemData: {FormatGearsetItemDataStruct(*gearsetData)}"); return ret; } // If you ever care to debug this, here is a formatted string output of this new gearsetData struct. - private string FormatGearsetItemDataStruct(GearsetDataStruct gearsetData) + private string FormatGearsetItemDataStruct(PacketPlayerGearsetData gearsetData) { string ret = $"\nMainhandWeaponData: Id: {gearsetData.MainhandWeaponData.Id}, Type: {gearsetData.MainhandWeaponData.Type}, " + $"Variant: {gearsetData.MainhandWeaponData.Variant}, Stain0: {gearsetData.MainhandWeaponData.Stain0}, Stain1: {gearsetData.MainhandWeaponData.Stain1}" + $"\nOffhandWeaponData: Id: {gearsetData.OffhandWeaponData.Id}, Type: {gearsetData.OffhandWeaponData.Type}, " + $"Variant: {gearsetData.OffhandWeaponData.Variant}, Stain0: {gearsetData.OffhandWeaponData.Stain0}, Stain1: {gearsetData.OffhandWeaponData.Stain1}" + - $"\nCrestBitField: {gearsetData.CrestBitField} | JobId: {gearsetData.JobId} | UNK_18: {gearsetData.UNK_18} | UNK_19: {gearsetData.UNK_19}"; + $"\nCrestBitField: {gearsetData.CrestBitField} | JobId: {gearsetData.JobId}"; for (int offset = 20; offset <= 56; offset += sizeof(LegacyCharacterArmor)) { LegacyCharacterArmor* equipSlotPtr = (LegacyCharacterArmor*)((byte*)&gearsetData + offset); @@ -156,43 +143,3 @@ public unsafe class UpdateSlotService : IDisposable return ret; } } - -// Can be removed once merged with client structs and referenced directly. See: https://github.com/aers/FFXIVClientStructs/pull/1277/files -[StructLayout(LayoutKind.Explicit)] -public readonly struct GearsetDataStruct -{ - // Stores the weapon data. Includes both dyes in the data. - [FieldOffset(0)] public readonly WeaponModelId MainhandWeaponData; - [FieldOffset(8)] public readonly WeaponModelId OffhandWeaponData; - - [FieldOffset(16)] public readonly byte CrestBitField; // A Bitfield:: ShieldCrest == 1, HeadCrest == 2, Chest Crest == 4 - [FieldOffset(17)] public readonly byte JobId; // Job ID associated with the gearset change. - - // Flicks from 0 to 127 (anywhere inbetween), have yet to associate what it is linked to. Remains the same when flicking between gearsets of the same job. - [FieldOffset(18)] public readonly byte UNK_18; - [FieldOffset(19)] public readonly byte UNK_19; // I have never seen this be anything other than 0. - - // Legacy helmet equip slot armor for a character. - [FieldOffset(20)] public readonly LegacyCharacterArmor HeadSlotArmor; - [FieldOffset(24)] public readonly LegacyCharacterArmor TopSlotArmor; - [FieldOffset(28)] public readonly LegacyCharacterArmor ArmsSlotArmor; - [FieldOffset(32)] public readonly LegacyCharacterArmor LegsSlotArmor; - [FieldOffset(26)] public readonly LegacyCharacterArmor FeetSlotArmor; - [FieldOffset(40)] public readonly LegacyCharacterArmor EarSlotArmor; - [FieldOffset(44)] public readonly LegacyCharacterArmor NeckSlotArmor; - [FieldOffset(48)] public readonly LegacyCharacterArmor WristSlotArmor; - [FieldOffset(52)] public readonly LegacyCharacterArmor RFingerSlotArmor; - [FieldOffset(56)] public readonly LegacyCharacterArmor LFingerSlotArmor; - - // Byte array of all slot's secondary dyes. - [FieldOffset(60)] public readonly byte HeadSlotSecondaryDye; - [FieldOffset(61)] public readonly byte TopSlotSecondaryDye; - [FieldOffset(62)] public readonly byte ArmsSlotSecondaryDye; - [FieldOffset(63)] public readonly byte LegsSlotSecondaryDye; - [FieldOffset(64)] public readonly byte FeetSlotSecondaryDye; - [FieldOffset(65)] public readonly byte EarSlotSecondaryDye; - [FieldOffset(66)] public readonly byte NeckSlotSecondaryDye; - [FieldOffset(67)] public readonly byte WristSlotSecondaryDye; - [FieldOffset(68)] public readonly byte RFingerSlotSecondaryDye; - [FieldOffset(69)] public readonly byte LFingerSlotSecondaryDye; -} diff --git a/Glamourer/State/StateEditor.cs b/Glamourer/State/StateEditor.cs index b122352..13b0706 100644 --- a/Glamourer/State/StateEditor.cs +++ b/Glamourer/State/StateEditor.cs @@ -416,7 +416,7 @@ public class StateEditor( var actors = settings.Source.RequiresChange() ? Applier.ApplyAll(state, requiresRedraw, false) : ActorData.Invalid; - + Glamourer.Log.Verbose( $"Applied design to {state.Identifier.Incognito(null)}. [Affecting {actors.ToLazyString("nothing")}.]"); StateChanged.Invoke(StateChangeType.Design, state.Sources[MetaIndex.Wetness], state, actors, null); // FIXME: maybe later diff --git a/Glamourer/State/StateListener.cs b/Glamourer/State/StateListener.cs index d570805..c4c16b5 100644 --- a/Glamourer/State/StateListener.cs +++ b/Glamourer/State/StateListener.cs @@ -225,7 +225,8 @@ public class StateListener : IDisposable // then we do not want to use our restricted gear protection // since we assume the player has that gear modded to availability. var locked = false; - if (actor.Identifier(_actors, out var identifier) && _manager.TryGetValue(identifier, out var state)) + if (actor.Identifier(_actors, out var identifier) + && _manager.TryGetValue(identifier, out var state)) { HandleEquipSlot(actor, state, slot, ref armor); locked = state.Sources[slot, false] is StateSource.IpcFixed; From 7be283ca30629e648725e4e1861616d86eda7238 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 24 Jan 2025 16:46:10 +0100 Subject: [PATCH 09/10] Apply API renames. --- Glamourer.Api | 2 +- Glamourer/Api/IpcProviders.cs | 2 +- Glamourer/Api/StateApi.cs | 8 ++++---- Glamourer/Events/StateUpdated.cs | 2 +- Glamourer/State/StateEditor.cs | 4 ++-- Glamourer/State/StateListener.cs | 2 +- Glamourer/State/StateManager.cs | 12 ++++++------ 7 files changed, 16 insertions(+), 16 deletions(-) diff --git a/Glamourer.Api b/Glamourer.Api index b1b90e6..4ac38fb 160000 --- a/Glamourer.Api +++ b/Glamourer.Api @@ -1 +1 @@ -Subproject commit b1b90e6ecfeee76a12cb27793753fa87af21083f +Subproject commit 4ac38fbed6fb0f31c0b75de26950ab82d3bee258 diff --git a/Glamourer/Api/IpcProviders.cs b/Glamourer/Api/IpcProviders.cs index 166245f..515cd34 100644 --- a/Glamourer/Api/IpcProviders.cs +++ b/Glamourer/Api/IpcProviders.cs @@ -52,7 +52,7 @@ public sealed class IpcProviders : IDisposable, IApiService IpcSubscribers.RevertToAutomationName.Provider(pi, api.State), IpcSubscribers.StateChanged.Provider(pi, api.State), IpcSubscribers.StateChangedWithType.Provider(pi, api.State), - IpcSubscribers.StateUpdated.Provider(pi, api.State), + IpcSubscribers.StateFinalized.Provider(pi, api.State), IpcSubscribers.GPoseChanged.Provider(pi, api.State), ]; _initializedProvider.Invoke(); diff --git a/Glamourer/Api/StateApi.cs b/Glamourer/Api/StateApi.cs index 41f7650..347d2b6 100644 --- a/Glamourer/Api/StateApi.cs +++ b/Glamourer/Api/StateApi.cs @@ -255,7 +255,7 @@ public sealed class StateApi : IGlamourerApiState, IApiService, IDisposable public event Action? StateChanged; public event Action? StateChangedWithType; - public event Action? StateUpdated; + public event Action? StateFinalized; public event Action? GPoseChanged; private void ApplyDesign(ActorState state, DesignBase design, uint key, ApplyFlag flags) @@ -349,11 +349,11 @@ public sealed class StateApi : IGlamourerApiState, IApiService, IDisposable StateChangedWithType.Invoke(actor.Address, type); } - private void OnStateUpdated(StateUpdateType type, ActorData actors) + private void OnStateUpdated(StateFinalizationType type, ActorData actors) { Glamourer.Log.Verbose($"[OnStateUpdated] State Updated with Type {type}. [Affecting {actors.ToLazyString("nothing")}.]"); - if (StateUpdated != null) + if (StateFinalized != null) foreach (var actor in actors.Objects) - StateUpdated.Invoke(actor.Address, type); + StateFinalized.Invoke(actor.Address, type); } } diff --git a/Glamourer/Events/StateUpdated.cs b/Glamourer/Events/StateUpdated.cs index f18a69a..faaf33a 100644 --- a/Glamourer/Events/StateUpdated.cs +++ b/Glamourer/Events/StateUpdated.cs @@ -14,7 +14,7 @@ namespace Glamourer.Events; /// /// public sealed class StateUpdated() - : EventWrapper(nameof(StateUpdated)) + : EventWrapper(nameof(StateUpdated)) { public enum Priority { diff --git a/Glamourer/State/StateEditor.cs b/Glamourer/State/StateEditor.cs index 13b0706..ebf347f 100644 --- a/Glamourer/State/StateEditor.cs +++ b/Glamourer/State/StateEditor.cs @@ -43,7 +43,7 @@ public class StateEditor( Glamourer.Log.Verbose( $"Set model id in state {state.Identifier.Incognito(null)} from {old} to {modelId}. [Affecting {actors.ToLazyString("nothing")}.]"); StateChanged.Invoke(StateChangeType.Model, source, state, actors, null); - StateUpdated.Invoke(StateUpdateType.ModelChange, actors); + StateUpdated.Invoke(StateFinalizationType.ModelChange, actors); } /// @@ -421,7 +421,7 @@ public class StateEditor( $"Applied design to {state.Identifier.Incognito(null)}. [Affecting {actors.ToLazyString("nothing")}.]"); StateChanged.Invoke(StateChangeType.Design, state.Sources[MetaIndex.Wetness], state, actors, null); // FIXME: maybe later if(settings.SendStateUpdate) - StateUpdated.Invoke(StateUpdateType.DesignApplied, actors); + StateUpdated.Invoke(StateFinalizationType.DesignApplied, actors); return; diff --git a/Glamourer/State/StateListener.cs b/Glamourer/State/StateListener.cs index c4c16b5..d8648bb 100644 --- a/Glamourer/State/StateListener.cs +++ b/Glamourer/State/StateListener.cs @@ -281,7 +281,7 @@ public class StateListener : IDisposable _objects.Update(); if (_objects.TryGetValue(identifier, out var actors) && actors.Valid) - _stateUpdated.Invoke(StateUpdateType.Gearset, actors); + _stateUpdated.Invoke(StateFinalizationType.Gearset, actors); } diff --git a/Glamourer/State/StateManager.cs b/Glamourer/State/StateManager.cs index 0348148..948e225 100644 --- a/Glamourer/State/StateManager.cs +++ b/Glamourer/State/StateManager.cs @@ -279,7 +279,7 @@ public sealed class StateManager( StateChanged.Invoke(StateChangeType.Reset, source, state, actors, null); // only invoke if we define this reset call as the final call in our state update. if(stateUpdate) - StateUpdated.Invoke(StateUpdateType.Revert, actors); + StateUpdated.Invoke(StateFinalizationType.Revert, actors); } public void ResetAdvancedState(ActorState state, StateSource source, uint key = 0) @@ -306,7 +306,7 @@ public sealed class StateManager( $"Reset advanced customization and dye state of {state.Identifier.Incognito(null)} to game base. [Affecting {actors.ToLazyString("nothing")}.]"); StateChanged.Invoke(StateChangeType.Reset, source, state, actors, null); // Update that we have completed a full operation. (We can do this directly as nothing else is linked) - StateUpdated.Invoke(StateUpdateType.RevertAdvanced, actors); + StateUpdated.Invoke(StateFinalizationType.RevertAdvanced, actors); } public void ResetCustomize(ActorState state, StateSource source, uint key = 0) @@ -325,7 +325,7 @@ public sealed class StateManager( Glamourer.Log.Verbose( $"Reset customization state of {state.Identifier.Incognito(null)} to game base. [Affecting {actors.ToLazyString("nothing")}.]"); // Update that we have completed a full operation. (We can do this directly as nothing else is linked) - StateUpdated.Invoke(StateUpdateType.RevertCustomize, actors); + StateUpdated.Invoke(StateFinalizationType.RevertCustomize, actors); } public void ResetEquip(ActorState state, StateSource source, uint key = 0) @@ -376,7 +376,7 @@ public sealed class StateManager( Glamourer.Log.Verbose( $"Reset equipment state of {state.Identifier.Incognito(null)} to game base. [Affecting {actors.ToLazyString("nothing")}.]"); // Update that we have completed a full operation. (We can do this directly as nothing else is linked) - StateUpdated.Invoke(StateUpdateType.RevertEquipment, actors); + StateUpdated.Invoke(StateFinalizationType.RevertEquipment, actors); } public void ResetStateFixed(ActorState state, bool respectManualPalettes, uint key = 0) @@ -469,7 +469,7 @@ public sealed class StateManager( || CustomizeArray.Compare(actor.Model.GetCustomize(), state.ModelData.Customize).RequiresRedraw(), false); StateChanged.Invoke(StateChangeType.Reapply, source, state, data, null); if(stateUpdate) - StateUpdated.Invoke(StateUpdateType.Reapply, data); + StateUpdated.Invoke(StateFinalizationType.Reapply, data); } /// Automation variant for reapply, to fire the correct StateUpdateType once reapplied. @@ -490,7 +490,7 @@ public sealed class StateManager( || CustomizeArray.Compare(actor.Model.GetCustomize(), state.ModelData.Customize).RequiresRedraw(), false); StateChanged.Invoke(StateChangeType.Reapply, source, state, data, null); // invoke the automation update based on what reset is. - StateUpdated.Invoke(wasReset ? StateUpdateType.RevertAutomation : StateUpdateType.ReapplyAutomation, data); + StateUpdated.Invoke(wasReset ? StateFinalizationType.RevertAutomation : StateFinalizationType.ReapplyAutomation, data); } public void DeleteState(ActorIdentifier identifier) From d9f9937d41390b34eab9d151e08f5b0e5d983789 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 24 Jan 2025 17:50:58 +0100 Subject: [PATCH 10/10] Continue renames, some cleanup. --- Glamourer/Api/DesignsApi.cs | 2 +- Glamourer/Api/IpcProviders.cs | 1 - Glamourer/Api/StateApi.cs | 40 +++---- Glamourer/Designs/IDesignEditor.cs | 8 +- Glamourer/Events/GPoseService.cs | 4 +- .../{StateUpdated.cs => StateFinalized.cs} | 13 +-- Glamourer/Gui/DesignQuickBar.cs | 4 +- Glamourer/Gui/Tabs/ActorTab/ActorPanel.cs | 8 +- .../Gui/Tabs/DebugTab/GlamourPlatePanel.cs | 2 +- .../Tabs/DebugTab/IpcTester/IpcTesterPanel.cs | 2 + .../Tabs/DebugTab/IpcTester/StateIpcTester.cs | 107 ++++++++++++------ Glamourer/Gui/Tabs/DesignTab/DesignPanel.cs | 4 +- Glamourer/Gui/Tabs/NpcTab/NpcPanel.cs | 4 +- Glamourer/Interop/UpdateSlotService.cs | 29 ++--- Glamourer/Services/CommandService.cs | 4 +- Glamourer/State/StateEditor.cs | 27 ++--- Glamourer/State/StateListener.cs | 8 +- Glamourer/State/StateManager.cs | 42 +++---- 18 files changed, 175 insertions(+), 134 deletions(-) rename Glamourer/Events/{StateUpdated.cs => StateFinalized.cs} (53%) diff --git a/Glamourer/Api/DesignsApi.cs b/Glamourer/Api/DesignsApi.cs index 6c3037e..08f5ddc 100644 --- a/Glamourer/Api/DesignsApi.cs +++ b/Glamourer/Api/DesignsApi.cs @@ -33,7 +33,7 @@ public class DesignsApi(ApiHelpers helpers, DesignManager designs, StateManager { var once = (flags & ApplyFlag.Once) != 0; var settings = new ApplySettings(Source: once ? StateSource.IpcManual : StateSource.IpcFixed, Key: key, MergeLinks: true, - ResetMaterials: !once && key != 0, SendStateUpdate: true); + ResetMaterials: !once && key != 0, IsFinal: true); using var restrict = ApiHelpers.Restrict(design, flags); stateManager.ApplyDesign(state, design, settings); diff --git a/Glamourer/Api/IpcProviders.cs b/Glamourer/Api/IpcProviders.cs index 515cd34..704f008 100644 --- a/Glamourer/Api/IpcProviders.cs +++ b/Glamourer/Api/IpcProviders.cs @@ -2,7 +2,6 @@ using Dalamud.Plugin; using Glamourer.Api.Api; using Glamourer.Api.Helpers; using OtterGui.Services; -using System.Reflection.Emit; using Glamourer.Api.Enums; namespace Glamourer.Api; diff --git a/Glamourer/Api/StateApi.cs b/Glamourer/Api/StateApi.cs index 347d2b6..eaf9d01 100644 --- a/Glamourer/Api/StateApi.cs +++ b/Glamourer/Api/StateApi.cs @@ -11,9 +11,9 @@ using OtterGui.Services; using Penumbra.GameData.Interop; using ObjectManager = Glamourer.Interop.ObjectManager; using StateChanged = Glamourer.Events.StateChanged; -using StateUpdated = Glamourer.Events.StateUpdated; namespace Glamourer.Api; + public sealed class StateApi : IGlamourerApiState, IApiService, IDisposable { private readonly ApiHelpers _helpers; @@ -23,7 +23,7 @@ public sealed class StateApi : IGlamourerApiState, IApiService, IDisposable private readonly AutoDesignApplier _autoDesigns; private readonly ObjectManager _objects; private readonly StateChanged _stateChanged; - private readonly StateUpdated _stateUpdated; + private readonly StateFinalized _stateFinalized; private readonly GPoseService _gPose; public StateApi(ApiHelpers helpers, @@ -33,27 +33,27 @@ public sealed class StateApi : IGlamourerApiState, IApiService, IDisposable AutoDesignApplier autoDesigns, ObjectManager objects, StateChanged stateChanged, - StateUpdated stateUpdated, + StateFinalized stateFinalized, GPoseService gPose) { - _helpers = helpers; - _stateManager = stateManager; - _converter = converter; - _config = config; - _autoDesigns = autoDesigns; - _objects = objects; - _stateChanged = stateChanged; - _stateUpdated = stateUpdated; - _gPose = gPose; + _helpers = helpers; + _stateManager = stateManager; + _converter = converter; + _config = config; + _autoDesigns = autoDesigns; + _objects = objects; + _stateChanged = stateChanged; + _stateFinalized = stateFinalized; + _gPose = gPose; _stateChanged.Subscribe(OnStateChanged, Events.StateChanged.Priority.GlamourerIpc); - _stateUpdated.Subscribe(OnStateUpdated, Events.StateUpdated.Priority.GlamourerIpc); - _gPose.Subscribe(OnGPoseChange, GPoseService.Priority.GlamourerIpc); + _stateFinalized.Subscribe(OnStateFinalized, Events.StateFinalized.Priority.StateApi); + _gPose.Subscribe(OnGPoseChange, GPoseService.Priority.StateApi); } public void Dispose() { _stateChanged.Unsubscribe(OnStateChanged); - _stateUpdated.Unsubscribe(OnStateUpdated); + _stateFinalized.Unsubscribe(OnStateFinalized); _gPose.Unsubscribe(OnGPoseChange); } @@ -253,16 +253,16 @@ public sealed class StateApi : IGlamourerApiState, IApiService, IDisposable return ApiHelpers.Return(GlamourerApiEc.Success, args); } - public event Action? StateChanged; - public event Action? StateChangedWithType; + public event Action? StateChanged; + public event Action? StateChangedWithType; public event Action? StateFinalized; - public event Action? GPoseChanged; + public event Action? GPoseChanged; private void ApplyDesign(ActorState state, DesignBase design, uint key, ApplyFlag flags) { var once = (flags & ApplyFlag.Once) != 0; var settings = new ApplySettings(Source: once ? StateSource.IpcManual : StateSource.IpcFixed, Key: key, MergeLinks: true, - ResetMaterials: !once && key != 0, SendStateUpdate: true); + ResetMaterials: !once && key != 0, IsFinal: true); _stateManager.ApplyDesign(state, design, settings); ApiHelpers.Lock(state, key, flags); } @@ -349,7 +349,7 @@ public sealed class StateApi : IGlamourerApiState, IApiService, IDisposable StateChangedWithType.Invoke(actor.Address, type); } - private void OnStateUpdated(StateFinalizationType type, ActorData actors) + private void OnStateFinalized(StateFinalizationType type, ActorData actors) { Glamourer.Log.Verbose($"[OnStateUpdated] State Updated with Type {type}. [Affecting {actors.ToLazyString("nothing")}.]"); if (StateFinalized != null) diff --git a/Glamourer/Designs/IDesignEditor.cs b/Glamourer/Designs/IDesignEditor.cs index 0178620..c18c98b 100644 --- a/Glamourer/Designs/IDesignEditor.cs +++ b/Glamourer/Designs/IDesignEditor.cs @@ -14,7 +14,7 @@ public readonly record struct ApplySettings( bool UseSingleSource = false, bool MergeLinks = false, bool ResetMaterials = false, - bool SendStateUpdate = false) + bool IsFinal = false) { public static readonly ApplySettings Manual = new() { @@ -25,7 +25,7 @@ public readonly record struct ApplySettings( UseSingleSource = false, MergeLinks = false, ResetMaterials = false, - SendStateUpdate = false, + IsFinal = false, }; public static readonly ApplySettings ManualWithLinks = new() @@ -37,7 +37,7 @@ public readonly record struct ApplySettings( UseSingleSource = false, MergeLinks = true, ResetMaterials = false, - SendStateUpdate = false, + IsFinal = false, }; public static readonly ApplySettings Game = new() @@ -49,7 +49,7 @@ public readonly record struct ApplySettings( UseSingleSource = false, MergeLinks = false, ResetMaterials = true, - SendStateUpdate = false, + IsFinal = false, }; } diff --git a/Glamourer/Events/GPoseService.cs b/Glamourer/Events/GPoseService.cs index a84f1d6..44421a0 100644 --- a/Glamourer/Events/GPoseService.cs +++ b/Glamourer/Events/GPoseService.cs @@ -13,8 +13,8 @@ public sealed class GPoseService : EventWrapper public enum Priority { - /// - GlamourerIpc = int.MinValue, + /// + StateApi = int.MinValue, } public bool InGPose { get; private set; } diff --git a/Glamourer/Events/StateUpdated.cs b/Glamourer/Events/StateFinalized.cs similarity index 53% rename from Glamourer/Events/StateUpdated.cs rename to Glamourer/Events/StateFinalized.cs index faaf33a..e8548e9 100644 --- a/Glamourer/Events/StateUpdated.cs +++ b/Glamourer/Events/StateFinalized.cs @@ -1,24 +1,23 @@ +using Glamourer.Api; using Glamourer.Api.Enums; -using Glamourer.Designs.History; using Glamourer.Interop.Structs; -using Glamourer.State; using OtterGui.Classes; namespace Glamourer.Events; /// -/// Triggered when a Design is edited in any way. +/// Triggered when a set of grouped changes finishes being applied to a Glamourer state. /// /// Parameter is the operation that finished updating the saved state. /// Parameter is the existing actors using this saved state. /// /// -public sealed class StateUpdated() - : EventWrapper(nameof(StateUpdated)) +public sealed class StateFinalized() + : EventWrapper(nameof(StateFinalized)) { public enum Priority { - /// - GlamourerIpc = int.MinValue, + /// + StateApi = int.MinValue, } } diff --git a/Glamourer/Gui/DesignQuickBar.cs b/Glamourer/Gui/DesignQuickBar.cs index bdc345f..50f86fd 100644 --- a/Glamourer/Gui/DesignQuickBar.cs +++ b/Glamourer/Gui/DesignQuickBar.cs @@ -183,7 +183,7 @@ public sealed class DesignQuickBar : Window, IDisposable } using var _ = design!.TemporarilyRestrictApplication(ApplicationCollection.FromKeys()); - _stateManager.ApplyDesign(state, design, ApplySettings.ManualWithLinks with { SendStateUpdate = true }); + _stateManager.ApplyDesign(state, design, ApplySettings.ManualWithLinks with { IsFinal = true }); } private void DrawRevertButton(Vector2 buttonSize) @@ -213,7 +213,7 @@ public sealed class DesignQuickBar : Window, IDisposable var (clicked, _, _, state) = ResolveTarget(FontAwesomeIcon.UndoAlt, buttonSize, tooltip, available); ImGui.SameLine(); if (clicked) - _stateManager.ResetState(state!, StateSource.Manual, stateUpdate: true); + _stateManager.ResetState(state!, StateSource.Manual, isFinal: true); } private void DrawRevertAutomationButton(Vector2 buttonSize) diff --git a/Glamourer/Gui/Tabs/ActorTab/ActorPanel.cs b/Glamourer/Gui/Tabs/ActorTab/ActorPanel.cs index ced78fb..d8f3cd1 100644 --- a/Glamourer/Gui/Tabs/ActorTab/ActorPanel.cs +++ b/Glamourer/Gui/Tabs/ActorTab/ActorPanel.cs @@ -385,7 +385,7 @@ public class ActorPanel { if (ImGuiUtil.DrawDisabledButton("Revert to Game", Vector2.Zero, "Revert the character to its actual state in the game.", _state!.IsLocked)) - _stateManager.ResetState(_state!, StateSource.Manual, stateUpdate: true); + _stateManager.ResetState(_state!, StateSource.Manual, isFinal: true); ImGui.SameLine(); @@ -423,7 +423,7 @@ public class ActorPanel if (_stateManager.GetOrCreate(id, data.Objects[0], out var state)) _stateManager.ApplyDesign(state, _converter.Convert(_state!, ApplicationRules.FromModifiers(_state!)), - ApplySettings.Manual with { SendStateUpdate = true }); + ApplySettings.Manual with { IsFinal = true }); } private void DrawApplyToTarget() @@ -440,7 +440,7 @@ public class ActorPanel if (_stateManager.GetOrCreate(id, data.Objects[0], out var state)) _stateManager.ApplyDesign(state, _converter.Convert(_state!, ApplicationRules.FromModifiers(_state!)), - ApplySettings.Manual with { SendStateUpdate = true }); + ApplySettings.Manual with { IsFinal = true }); } @@ -467,7 +467,7 @@ public class ActorPanel var text = ImGui.GetClipboardText(); var design = panel._converter.FromBase64(text, applyCustomize, applyGear, out _) ?? throw new Exception("The clipboard did not contain valid data."); - panel._stateManager.ApplyDesign(panel._state!, design, ApplySettings.ManualWithLinks with { SendStateUpdate = true }); + panel._stateManager.ApplyDesign(panel._state!, design, ApplySettings.ManualWithLinks with { IsFinal = true }); } catch (Exception ex) { diff --git a/Glamourer/Gui/Tabs/DebugTab/GlamourPlatePanel.cs b/Glamourer/Gui/Tabs/DebugTab/GlamourPlatePanel.cs index c44a722..62f93e9 100644 --- a/Glamourer/Gui/Tabs/DebugTab/GlamourPlatePanel.cs +++ b/Glamourer/Gui/Tabs/DebugTab/GlamourPlatePanel.cs @@ -79,7 +79,7 @@ public unsafe class GlamourPlatePanel : IGameDataDrawer if (ImGuiUtil.DrawDisabledButton("Apply to Player", Vector2.Zero, string.Empty, !enabled)) { var design = CreateDesign(plate); - _state.ApplyDesign(state!, design, ApplySettings.Manual with { SendStateUpdate = true }); + _state.ApplyDesign(state!, design, ApplySettings.Manual with { IsFinal = true }); } using (ImRaii.Group()) diff --git a/Glamourer/Gui/Tabs/DebugTab/IpcTester/IpcTesterPanel.cs b/Glamourer/Gui/Tabs/DebugTab/IpcTester/IpcTesterPanel.cs index 8f561af..1a78b24 100644 --- a/Glamourer/Gui/Tabs/DebugTab/IpcTester/IpcTesterPanel.cs +++ b/Glamourer/Gui/Tabs/DebugTab/IpcTester/IpcTesterPanel.cs @@ -51,6 +51,7 @@ public class IpcTesterPanel( Glamourer.Log.Debug("[IPCTester] Subscribed to IPC events for IPC tester."); state.GPoseChanged.Enable(); state.StateChanged.Enable(); + state.StateFinalized.Enable(); framework.Update += CheckUnsubscribe; _subscribed = true; } @@ -73,5 +74,6 @@ public class IpcTesterPanel( _subscribed = false; state.GPoseChanged.Disable(); state.StateChanged.Disable(); + state.StateFinalized.Disable(); } } diff --git a/Glamourer/Gui/Tabs/DebugTab/IpcTester/StateIpcTester.cs b/Glamourer/Gui/Tabs/DebugTab/IpcTester/StateIpcTester.cs index f378625..638bffc 100644 --- a/Glamourer/Gui/Tabs/DebugTab/IpcTester/StateIpcTester.cs +++ b/Glamourer/Gui/Tabs/DebugTab/IpcTester/StateIpcTester.cs @@ -11,6 +11,7 @@ using Newtonsoft.Json.Linq; using OtterGui; using OtterGui.Raii; using OtterGui.Services; +using OtterGui.Text; using Penumbra.GameData.Interop; using Penumbra.String; @@ -31,9 +32,16 @@ public class StateIpcTester : IUiService, IDisposable private string? _getStateString; public readonly EventSubscriber StateChanged; - private nint _lastStateChangeActor; - private ByteString _lastStateChangeName = ByteString.Empty; - private DateTime _lastStateChangeTime; + private nint _lastStateChangeActor; + private ByteString _lastStateChangeName = ByteString.Empty; + private DateTime _lastStateChangeTime; + private StateChangeType _lastStateChangeType; + + public readonly EventSubscriber StateFinalized; + private nint _lastStateFinalizeActor; + private ByteString _lastStateFinalizeName = ByteString.Empty; + private DateTime _lastStateFinalizeTime; + private StateFinalizationType _lastStateFinalizeType; public readonly EventSubscriber GPoseChanged; private bool _lastGPoseChangeValue; @@ -44,15 +52,18 @@ public class StateIpcTester : IUiService, IDisposable public StateIpcTester(IDalamudPluginInterface pluginInterface) { _pluginInterface = pluginInterface; - StateChanged = Api.IpcSubscribers.StateChangedWithType.Subscriber(_pluginInterface, OnStateChanged); + StateChanged = StateChangedWithType.Subscriber(_pluginInterface, OnStateChanged); + StateFinalized = Api.IpcSubscribers.StateFinalized.Subscriber(_pluginInterface, OnStateFinalized); GPoseChanged = Api.IpcSubscribers.GPoseChanged.Subscriber(_pluginInterface, OnGPoseChange); StateChanged.Disable(); + StateFinalized.Disable(); GPoseChanged.Disable(); } public void Dispose() { StateChanged.Dispose(); + StateFinalized.Dispose(); GPoseChanged.Dispose(); } @@ -73,86 +84,88 @@ public class StateIpcTester : IUiService, IDisposable IpcTesterHelpers.DrawIntro("Last Error"); ImGui.TextUnformatted(_lastError.ToString()); IpcTesterHelpers.DrawIntro("Last State Change"); - PrintName(); + PrintChangeName(); + IpcTesterHelpers.DrawIntro("Last State Finalization"); + PrintFinalizeName(); IpcTesterHelpers.DrawIntro("Last GPose Change"); ImGui.TextUnformatted($"{_lastGPoseChangeValue} at {_lastGPoseChangeTime.ToLocalTime().TimeOfDay}"); IpcTesterHelpers.DrawIntro(GetState.Label); DrawStatePopup(); - if (ImGui.Button("Get##Idx")) + if (ImUtf8.Button("Get##Idx"u8)) { (_lastError, _state) = new GetState(_pluginInterface).Invoke(_gameObjectIndex, _key); _stateString = _state?.ToString(Formatting.Indented) ?? "No State Available"; - ImGui.OpenPopup("State"); + ImUtf8.OpenPopup("State"u8); } IpcTesterHelpers.DrawIntro(GetStateName.Label); - if (ImGui.Button("Get##Name")) + if (ImUtf8.Button("Get##Name"u8)) { (_lastError, _state) = new GetStateName(_pluginInterface).Invoke(_gameObjectName, _key); _stateString = _state?.ToString(Formatting.Indented) ?? "No State Available"; - ImGui.OpenPopup("State"); + ImUtf8.OpenPopup("State"u8); } IpcTesterHelpers.DrawIntro(GetStateBase64.Label); - if (ImGui.Button("Get##Base64Idx")) + if (ImUtf8.Button("Get##Base64Idx"u8)) { (_lastError, _getStateString) = new GetStateBase64(_pluginInterface).Invoke(_gameObjectIndex, _key); _stateString = _getStateString ?? "No State Available"; - ImGui.OpenPopup("State"); + ImUtf8.OpenPopup("State"u8); } IpcTesterHelpers.DrawIntro(GetStateBase64Name.Label); - if (ImGui.Button("Get##Base64Idx")) + if (ImUtf8.Button("Get##Base64Idx"u8)) { (_lastError, _getStateString) = new GetStateBase64Name(_pluginInterface).Invoke(_gameObjectName, _key); _stateString = _getStateString ?? "No State Available"; - ImGui.OpenPopup("State"); + ImUtf8.OpenPopup("State"u8); } IpcTesterHelpers.DrawIntro(ApplyState.Label); if (ImGuiUtil.DrawDisabledButton("Apply Last##Idx", Vector2.Zero, string.Empty, _state == null)) _lastError = new ApplyState(_pluginInterface).Invoke(_state!, _gameObjectIndex, _key, _flags); ImGui.SameLine(); - if (ImGui.Button("Apply Base64##Idx")) + if (ImUtf8.Button("Apply Base64##Idx"u8)) _lastError = new ApplyState(_pluginInterface).Invoke(_base64State, _gameObjectIndex, _key, _flags); IpcTesterHelpers.DrawIntro(ApplyStateName.Label); if (ImGuiUtil.DrawDisabledButton("Apply Last##Name", Vector2.Zero, string.Empty, _state == null)) _lastError = new ApplyStateName(_pluginInterface).Invoke(_state!, _gameObjectName, _key, _flags); ImGui.SameLine(); - if (ImGui.Button("Apply Base64##Name")) + if (ImUtf8.Button("Apply Base64##Name"u8)) _lastError = new ApplyStateName(_pluginInterface).Invoke(_base64State, _gameObjectName, _key, _flags); IpcTesterHelpers.DrawIntro(RevertState.Label); - if (ImGui.Button("Revert##Idx")) + if (ImUtf8.Button("Revert##Idx"u8)) _lastError = new RevertState(_pluginInterface).Invoke(_gameObjectIndex, _key, _flags); IpcTesterHelpers.DrawIntro(RevertStateName.Label); - if (ImGui.Button("Revert##Name")) + if (ImUtf8.Button("Revert##Name"u8)) _lastError = new RevertStateName(_pluginInterface).Invoke(_gameObjectName, _key, _flags); IpcTesterHelpers.DrawIntro(UnlockState.Label); - if (ImGui.Button("Unlock##Idx")) + if (ImUtf8.Button("Unlock##Idx"u8)) _lastError = new UnlockState(_pluginInterface).Invoke(_gameObjectIndex, _key); IpcTesterHelpers.DrawIntro(UnlockStateName.Label); - if (ImGui.Button("Unlock##Name")) + if (ImUtf8.Button("Unlock##Name"u8)) _lastError = new UnlockStateName(_pluginInterface).Invoke(_gameObjectName, _key); IpcTesterHelpers.DrawIntro(UnlockAll.Label); - if (ImGui.Button("Unlock##All")) + if (ImUtf8.Button("Unlock##All"u8)) _numUnlocked = new UnlockAll(_pluginInterface).Invoke(_key); ImGui.SameLine(); ImGui.TextUnformatted($"Unlocked {_numUnlocked}"); IpcTesterHelpers.DrawIntro(RevertToAutomation.Label); - if (ImGui.Button("Revert##AutomationIdx")) + if (ImUtf8.Button("Revert##AutomationIdx"u8)) _lastError = new RevertToAutomation(_pluginInterface).Invoke(_gameObjectIndex, _key, _flags); IpcTesterHelpers.DrawIntro(RevertToAutomationName.Label); - if (ImGui.Button("Revert##AutomationName")) + if (ImUtf8.Button("Revert##AutomationName"u8)) _lastError = new RevertToAutomationName(_pluginInterface).Invoke(_gameObjectName, _key, _flags); } @@ -162,44 +175,70 @@ public class StateIpcTester : IUiService, IDisposable if (_stateString == null) return; - using var p = ImRaii.Popup("State"); + using var p = ImUtf8.Popup("State"u8); if (!p) return; - if (ImGui.Button("Copy to Clipboard")) - ImGui.SetClipboardText(_stateString); + if (ImUtf8.Button("Copy to Clipboard"u8)) + ImUtf8.SetClipboardText(_stateString); if (_stateString[0] is '{') { ImGui.SameLine(); - if (ImGui.Button("Copy as Base64") && _state != null) - ImGui.SetClipboardText(DesignConverter.ToBase64(_state)); + if (ImUtf8.Button("Copy as Base64"u8) && _state != null) + ImUtf8.SetClipboardText(DesignConverter.ToBase64(_state)); } using var font = ImRaii.PushFont(UiBuilder.MonoFont); - ImGuiUtil.TextWrapped(_stateString ?? string.Empty); + ImUtf8.TextWrapped(_stateString ?? string.Empty); - if (ImGui.Button("Close", -Vector2.UnitX) || !ImGui.IsWindowFocused()) + if (ImUtf8.Button("Close"u8, -Vector2.UnitX) || !ImGui.IsWindowFocused()) ImGui.CloseCurrentPopup(); } - private unsafe void PrintName() + private unsafe void PrintChangeName() { - ImGuiNative.igTextUnformatted(_lastStateChangeName.Path, _lastStateChangeName.Path + _lastStateChangeName.Length); + ImUtf8.Text(_lastStateChangeName.Span); + ImGui.SameLine(0, 0); + ImUtf8.Text($" ({_lastStateChangeType})"); ImGui.SameLine(); using (ImRaii.PushFont(UiBuilder.MonoFont)) { - ImGuiUtil.CopyOnClickSelectable($"0x{_lastStateChangeActor:X}"); + ImUtf8.CopyOnClickSelectable($"0x{_lastStateChangeActor:X}"); } ImGui.SameLine(); - ImGui.TextUnformatted($"at {_lastStateChangeTime.ToLocalTime().TimeOfDay}"); + ImUtf8.Text($"at {_lastStateChangeTime.ToLocalTime().TimeOfDay}"); } - private void OnStateChanged(nint actor, StateChangeType _) + private unsafe void PrintFinalizeName() + { + ImUtf8.Text(_lastStateFinalizeName.Span); + ImGui.SameLine(0, 0); + ImUtf8.Text($" ({_lastStateFinalizeType})"); + ImGui.SameLine(); + using (ImRaii.PushFont(UiBuilder.MonoFont)) + { + ImUtf8.CopyOnClickSelectable($"0x{_lastStateFinalizeActor:X}"); + } + + ImGui.SameLine(); + ImUtf8.Text($"at {_lastStateFinalizeTime.ToLocalTime().TimeOfDay}"); + } + + private void OnStateChanged(nint actor, StateChangeType type) { _lastStateChangeActor = actor; _lastStateChangeTime = DateTime.UtcNow; _lastStateChangeName = actor != nint.Zero ? ((Actor)actor).Utf8Name.Clone() : ByteString.Empty; + _lastStateChangeType = type; + } + + private void OnStateFinalized(nint actor, StateFinalizationType type) + { + _lastStateFinalizeActor = actor; + _lastStateFinalizeTime = DateTime.UtcNow; + _lastStateFinalizeName = actor != nint.Zero ? ((Actor)actor).Utf8Name.Clone() : ByteString.Empty; + _lastStateFinalizeType = type; } private void OnGPoseChange(bool value) diff --git a/Glamourer/Gui/Tabs/DesignTab/DesignPanel.cs b/Glamourer/Gui/Tabs/DesignTab/DesignPanel.cs index fe71609..42eb8e9 100644 --- a/Glamourer/Gui/Tabs/DesignTab/DesignPanel.cs +++ b/Glamourer/Gui/Tabs/DesignTab/DesignPanel.cs @@ -460,7 +460,7 @@ public class DesignPanel if (_state.GetOrCreate(id, data.Objects[0], out var state)) { using var _ = _selector.Selected!.TemporarilyRestrictApplication(ApplicationCollection.FromKeys()); - _state.ApplyDesign(state, _selector.Selected!, ApplySettings.ManualWithLinks with { SendStateUpdate = true }); + _state.ApplyDesign(state, _selector.Selected!, ApplySettings.ManualWithLinks with { IsFinal = true }); } } @@ -478,7 +478,7 @@ public class DesignPanel if (_state.GetOrCreate(id, data.Objects[0], out var state)) { using var _ = _selector.Selected!.TemporarilyRestrictApplication(ApplicationCollection.FromKeys()); - _state.ApplyDesign(state, _selector.Selected!, ApplySettings.ManualWithLinks with { SendStateUpdate = true }); + _state.ApplyDesign(state, _selector.Selected!, ApplySettings.ManualWithLinks with { IsFinal = true }); } } diff --git a/Glamourer/Gui/Tabs/NpcTab/NpcPanel.cs b/Glamourer/Gui/Tabs/NpcTab/NpcPanel.cs index 345df11..aeb96f6 100644 --- a/Glamourer/Gui/Tabs/NpcTab/NpcPanel.cs +++ b/Glamourer/Gui/Tabs/NpcTab/NpcPanel.cs @@ -196,7 +196,7 @@ public class NpcPanel if (_state.GetOrCreate(id, data.Objects[0], out var state)) { var design = _converter.Convert(ToDesignData(), new StateMaterialManager(), ApplicationRules.NpcFromModifiers()); - _state.ApplyDesign(state, design, ApplySettings.Manual with { SendStateUpdate = true }); + _state.ApplyDesign(state, design, ApplySettings.Manual with { IsFinal = true }); } } @@ -214,7 +214,7 @@ public class NpcPanel if (_state.GetOrCreate(id, data.Objects[0], out var state)) { var design = _converter.Convert(ToDesignData(), new StateMaterialManager(), ApplicationRules.NpcFromModifiers()); - _state.ApplyDesign(state, design, ApplySettings.Manual with { SendStateUpdate = true }); + _state.ApplyDesign(state, design, ApplySettings.Manual with { IsFinal = true }); } } diff --git a/Glamourer/Interop/UpdateSlotService.cs b/Glamourer/Interop/UpdateSlotService.cs index 466f1ae..ffa6581 100644 --- a/Glamourer/Interop/UpdateSlotService.cs +++ b/Glamourer/Interop/UpdateSlotService.cs @@ -18,6 +18,7 @@ public unsafe class UpdateSlotService : IDisposable public readonly BonusSlotUpdating BonusSlotUpdatingEvent; public readonly GearsetDataLoaded GearsetDataLoadedEvent; private readonly DictBonusItems _bonusItems; + public UpdateSlotService(EquipSlotUpdating equipSlotUpdating, BonusSlotUpdating bonusSlotUpdating, GearsetDataLoaded gearsetDataLoaded, IGameInteropProvider interop, DictBonusItems bonusItems) { @@ -26,8 +27,8 @@ public unsafe class UpdateSlotService : IDisposable GearsetDataLoadedEvent = gearsetDataLoaded; _bonusItems = bonusItems; - _loadGearsetDataHook = interop.HookFromAddress((nint)DrawDataContainer.MemberFunctionPointers.LoadGearsetData, LoadGearsetDataDetour); interop.InitializeFromAttributes(this); + _loadGearsetDataHook = interop.HookFromAddress((nint)DrawDataContainer.MemberFunctionPointers.LoadGearsetData, LoadGearsetDataDetour); _flagSlotForUpdateHook.Enable(); _flagBonusSlotForUpdateHook.Enable(); _loadGearsetDataHook.Enable(); @@ -89,8 +90,8 @@ public unsafe class UpdateSlotService : IDisposable /// Detours the func that makes all FlagSlotForUpdate calls on a gearset change or initial render of a given actor (Only Cases this is Called). /// Logic done after returning the original hook executes After all equipment/weapon/crest data is loaded into the Actors BaseData. /// - private delegate Int64 LoadGearsetDataDelegate(DrawDataContainer* drawDataContainer, PacketPlayerGearsetData* gearsetData); - private readonly Hook _loadGearsetDataHook = null!; + private delegate ulong LoadGearsetDataDelegate(DrawDataContainer* drawDataContainer, PacketPlayerGearsetData* gearsetData); + private readonly Hook _loadGearsetDataHook; private ulong FlagSlotForUpdateDetour(nint drawObject, uint slotIdx, CharacterArmor* data) { @@ -115,30 +116,30 @@ public unsafe class UpdateSlotService : IDisposable Glamourer.Log.Excessive($"[FlagBonusSlotForUpdate] Glamourer-Invoked on 0x{drawObject.Address:X} on {slot} with item data {armor}."); return _flagSlotForUpdateHook.Original(drawObject.Address, slot.ToIndex(), &armor); } - private Int64 LoadGearsetDataDetour(DrawDataContainer* drawDataContainer, PacketPlayerGearsetData* gearsetData) + private ulong LoadGearsetDataDetour(DrawDataContainer* drawDataContainer, PacketPlayerGearsetData* gearsetData) { var ret = _loadGearsetDataHook.Original(drawDataContainer, gearsetData); - Model drawObject = drawDataContainer->OwnerObject->DrawObject; + var drawObject = drawDataContainer->OwnerObject->DrawObject; GearsetDataLoadedEvent.Invoke(drawObject); - // Glamourer.Log.Excessive($"[LoadAllEquipmentDetour] GearsetItemData: {FormatGearsetItemDataStruct(*gearsetData)}"); + Glamourer.Log.Excessive($"[LoadAllEquipmentDetour] GearsetItemData: {FormatGearsetItemDataStruct(*gearsetData)}"); return ret; } - // If you ever care to debug this, here is a formatted string output of this new gearsetData struct. - private string FormatGearsetItemDataStruct(PacketPlayerGearsetData gearsetData) + + private static string FormatGearsetItemDataStruct(PacketPlayerGearsetData gearsetData) { - string ret = + var ret = $"\nMainhandWeaponData: Id: {gearsetData.MainhandWeaponData.Id}, Type: {gearsetData.MainhandWeaponData.Type}, " + $"Variant: {gearsetData.MainhandWeaponData.Variant}, Stain0: {gearsetData.MainhandWeaponData.Stain0}, Stain1: {gearsetData.MainhandWeaponData.Stain1}" + $"\nOffhandWeaponData: Id: {gearsetData.OffhandWeaponData.Id}, Type: {gearsetData.OffhandWeaponData.Type}, " + $"Variant: {gearsetData.OffhandWeaponData.Variant}, Stain0: {gearsetData.OffhandWeaponData.Stain0}, Stain1: {gearsetData.OffhandWeaponData.Stain1}" + $"\nCrestBitField: {gearsetData.CrestBitField} | JobId: {gearsetData.JobId}"; - for (int offset = 20; offset <= 56; offset += sizeof(LegacyCharacterArmor)) + for (var offset = 20; offset <= 56; offset += sizeof(LegacyCharacterArmor)) { - LegacyCharacterArmor* equipSlotPtr = (LegacyCharacterArmor*)((byte*)&gearsetData + offset); - int dyeOffset = (offset - 20) / sizeof(LegacyCharacterArmor) + 60; // Calculate the corresponding dye offset - byte* dyePtr = (byte*)&gearsetData + dyeOffset; - ret += $"\nEquipSlot {((EquipSlot)(dyeOffset - 60)).ToString()}:: Id: {(*equipSlotPtr).Set}, Variant: {(*equipSlotPtr).Variant}, Stain0: {(*equipSlotPtr).Stain.Id}, Stain1: {*dyePtr}"; + var equipSlotPtr = (LegacyCharacterArmor*)((byte*)&gearsetData + offset); + var dyeOffset = (offset - 20) / sizeof(LegacyCharacterArmor) + 60; // Calculate the corresponding dye offset + var dyePtr = (byte*)&gearsetData + dyeOffset; + ret += $"\nEquipSlot {(EquipSlot)(dyeOffset - 60)}:: Id: {(*equipSlotPtr).Set}, Variant: {(*equipSlotPtr).Variant}, Stain0: {(*equipSlotPtr).Stain.Id}, Stain1: {*dyePtr}"; } return ret; } diff --git a/Glamourer/Services/CommandService.cs b/Glamourer/Services/CommandService.cs index a869a09..98dfa19 100644 --- a/Glamourer/Services/CommandService.cs +++ b/Glamourer/Services/CommandService.cs @@ -668,7 +668,7 @@ public class CommandService : IDisposable, IApiService if (!_objects.TryGetValue(identifier, out var actors)) { if (_stateManager.TryGetValue(identifier, out var state)) - _stateManager.ApplyDesign(state, design, ApplySettings.ManualWithLinks with { SendStateUpdate = true }); + _stateManager.ApplyDesign(state, design, ApplySettings.ManualWithLinks with { IsFinal = true }); } else { @@ -677,7 +677,7 @@ public class CommandService : IDisposable, IApiService if (_stateManager.GetOrCreate(actor.GetIdentifier(_actors), actor, out var state)) { ApplyModSettings(design, actor, applyMods); - _stateManager.ApplyDesign(state, design, ApplySettings.ManualWithLinks with { SendStateUpdate = true }); + _stateManager.ApplyDesign(state, design, ApplySettings.ManualWithLinks with { IsFinal = true }); } } } diff --git a/Glamourer/State/StateEditor.cs b/Glamourer/State/StateEditor.cs index ebf347f..42058d2 100644 --- a/Glamourer/State/StateEditor.cs +++ b/Glamourer/State/StateEditor.cs @@ -17,7 +17,7 @@ public class StateEditor( InternalStateEditor editor, StateApplier applier, StateChanged stateChanged, - StateUpdated stateUpdated, + StateFinalized stateFinalized, JobChangeState jobChange, Configuration config, ItemManager items, @@ -25,12 +25,12 @@ public class StateEditor( ModSettingApplier modApplier, GPoseService gPose) : IDesignEditor { - protected readonly InternalStateEditor Editor = editor; - protected readonly StateApplier Applier = applier; - protected readonly StateChanged StateChanged = stateChanged; - protected readonly StateUpdated StateUpdated = stateUpdated; - protected readonly Configuration Config = config; - protected readonly ItemManager Items = items; + protected readonly InternalStateEditor Editor = editor; + protected readonly StateApplier Applier = applier; + protected readonly StateChanged StateChanged = stateChanged; + protected readonly StateFinalized StateFinalized = stateFinalized; + protected readonly Configuration Config = config; + protected readonly ItemManager Items = items; /// Turn an actor to. public void ChangeModelId(ActorState state, uint modelId, CustomizeArray customize, nint equipData, StateSource source, @@ -43,7 +43,7 @@ public class StateEditor( Glamourer.Log.Verbose( $"Set model id in state {state.Identifier.Incognito(null)} from {old} to {modelId}. [Affecting {actors.ToLazyString("nothing")}.]"); StateChanged.Invoke(StateChangeType.Model, source, state, actors, null); - StateUpdated.Invoke(StateFinalizationType.ModelChange, actors); + StateFinalized.Invoke(StateFinalizationType.ModelChange, actors); } /// @@ -383,7 +383,7 @@ public class StateEditor( Editor.ChangeMetaState(state, meta, mergedDesign.Design.DesignData.GetMeta(meta), Source(meta), out _, settings.Key); } - if (settings.ResetMaterials || (!settings.RespectManual && mergedDesign.ResetAdvancedDyes)) + if (settings.ResetMaterials || !settings.RespectManual && mergedDesign.ResetAdvancedDyes) state.Materials.Clear(); foreach (var (key, value) in mergedDesign.Design.Materials) @@ -420,8 +420,8 @@ public class StateEditor( Glamourer.Log.Verbose( $"Applied design to {state.Identifier.Incognito(null)}. [Affecting {actors.ToLazyString("nothing")}.]"); StateChanged.Invoke(StateChangeType.Design, state.Sources[MetaIndex.Wetness], state, actors, null); // FIXME: maybe later - if(settings.SendStateUpdate) - StateUpdated.Invoke(StateFinalizationType.DesignApplied, actors); + if (settings.IsFinal) + StateFinalized.Invoke(StateFinalizationType.DesignApplied, actors); return; @@ -442,7 +442,8 @@ public class StateEditor( if (!settings.MergeLinks || design is not Design d) merged = new MergedDesign(design); else - merged = merger.Merge(d.AllLinks(true), state.ModelData.IsHuman ? state.ModelData.Customize : CustomizeArray.Default, state.BaseData, + merged = merger.Merge(d.AllLinks(true), state.ModelData.IsHuman ? state.ModelData.Customize : CustomizeArray.Default, + state.BaseData, false, Config.AlwaysApplyAssociatedMods); ApplyDesign(data, merged, settings with @@ -460,7 +461,7 @@ public class StateEditor( if (!Config.ChangeEntireItem || !settings.Source.IsManual()) return; - var mh = newMainhand ?? state.ModelData.Item(EquipSlot.MainHand); + var mh = newMainhand ?? state.ModelData.Item(EquipSlot.MainHand); // Do not change Shields to nothing. if (mh.Type is FullEquipType.Sword) return; diff --git a/Glamourer/State/StateListener.cs b/Glamourer/State/StateListener.cs index d8648bb..3a6d6ef 100644 --- a/Glamourer/State/StateListener.cs +++ b/Glamourer/State/StateListener.cs @@ -41,7 +41,7 @@ public class StateListener : IDisposable private readonly HeadGearVisibilityChanged _headGearVisibility; private readonly VisorStateChanged _visorState; private readonly WeaponVisibilityChanged _weaponVisibility; - private readonly StateUpdated _stateUpdated; + private readonly StateFinalized _stateFinalized; private readonly AutoDesignApplier _autoDesignApplier; private readonly FunModule _funModule; private readonly HumanModelList _humans; @@ -63,7 +63,7 @@ public class StateListener : IDisposable WeaponVisibilityChanged weaponVisibility, HeadGearVisibilityChanged headGearVisibility, AutoDesignApplier autoDesignApplier, FunModule funModule, HumanModelList humans, StateApplier applier, MovedEquipment movedEquipment, ObjectManager objects, GPoseService gPose, ChangeCustomizeService changeCustomizeService, CustomizeService customizations, ICondition condition, - CrestService crestService, BonusSlotUpdating bonusSlotUpdating, StateUpdated stateUpdated) + CrestService crestService, BonusSlotUpdating bonusSlotUpdating, StateFinalized stateFinalized) { _manager = manager; _items = items; @@ -88,7 +88,7 @@ public class StateListener : IDisposable _condition = condition; _crestService = crestService; _bonusSlotUpdating = bonusSlotUpdating; - _stateUpdated = stateUpdated; + _stateFinalized = stateFinalized; Subscribe(); } @@ -281,7 +281,7 @@ public class StateListener : IDisposable _objects.Update(); if (_objects.TryGetValue(identifier, out var actors) && actors.Valid) - _stateUpdated.Invoke(StateFinalizationType.Gearset, actors); + _stateFinalized.Invoke(StateFinalizationType.Gearset, actors); } diff --git a/Glamourer/State/StateManager.cs b/Glamourer/State/StateManager.cs index 948e225..9b71586 100644 --- a/Glamourer/State/StateManager.cs +++ b/Glamourer/State/StateManager.cs @@ -18,20 +18,20 @@ using Penumbra.GameData.Interop; namespace Glamourer.State; public sealed class StateManager( - ActorManager _actors, + ActorManager actors, ItemManager items, - StateChanged @event, - StateUpdated @event2, + StateChanged changeEvent, + StateFinalized finalizeEvent, StateApplier applier, InternalStateEditor editor, - HumanModelList _humans, - IClientState _clientState, + HumanModelList humans, + IClientState clientState, Configuration config, JobChangeState jobChange, DesignMerger merger, ModSettingApplier modApplier, GPoseService gPose) - : StateEditor(editor, applier, @event, @event2, jobChange, config, items, merger, modApplier, gPose), + : StateEditor(editor, applier, changeEvent, finalizeEvent, jobChange, config, items, merger, modApplier, gPose), IReadOnlyDictionary { private readonly Dictionary _states = []; @@ -62,7 +62,7 @@ public sealed class StateManager( /// public bool GetOrCreate(Actor actor, [NotNullWhen(true)] out ActorState? state) - => GetOrCreate(actor.GetIdentifier(_actors), actor, out state); + => GetOrCreate(actor.GetIdentifier(actors), actor, out state); /// Try to obtain or create a new state for an existing actor. Returns false if no state could be created. public unsafe bool GetOrCreate(ActorIdentifier identifier, Actor actor, [NotNullWhen(true)] out ActorState? state) @@ -82,7 +82,7 @@ public sealed class StateManager( ModelData = FromActor(actor, true, false), BaseData = FromActor(actor, false, false), LastJob = (byte)(actor.IsCharacter ? actor.AsCharacter->CharacterData.ClassJob : 0), - LastTerritory = _clientState.TerritoryType, + LastTerritory = clientState.TerritoryType, }; // state.Identifier is owned. _states.Add(state.Identifier, state); @@ -115,7 +115,7 @@ public sealed class StateManager( // Model ID is only unambiguously contained in the game object. // The draw object only has the object type. // TODO reverse search model data to get model id from model. - if (!_humans.IsHuman((uint)actor.AsCharacter->ModelContainer.ModelCharaId)) + if (!humans.IsHuman((uint)actor.AsCharacter->ModelContainer.ModelCharaId)) { ret.LoadNonHuman((uint)actor.AsCharacter->ModelContainer.ModelCharaId, *(CustomizeArray*)&actor.AsCharacter->DrawData.CustomizeData, (nint)Unsafe.AsPointer(ref actor.AsCharacter->DrawData.EquipmentModelIds[0])); @@ -236,7 +236,7 @@ public sealed class StateManager( public void TurnHuman(ActorState state, StateSource source, uint key = 0) => ChangeModelId(state, 0, CustomizeArray.Default, nint.Zero, source, key); - public void ResetState(ActorState state, StateSource source, uint key = 0, bool stateUpdate = false) + public void ResetState(ActorState state, StateSource source, uint key = 0, bool isFinal = false) { if (!state.Unlock(key)) return; @@ -278,8 +278,8 @@ public sealed class StateManager( $"Reset entire state of {state.Identifier.Incognito(null)} to game base. [Affecting {actors.ToLazyString("nothing")}.]"); StateChanged.Invoke(StateChangeType.Reset, source, state, actors, null); // only invoke if we define this reset call as the final call in our state update. - if(stateUpdate) - StateUpdated.Invoke(StateFinalizationType.Revert, actors); + if(isFinal) + StateFinalized.Invoke(StateFinalizationType.Revert, actors); } public void ResetAdvancedState(ActorState state, StateSource source, uint key = 0) @@ -306,7 +306,7 @@ public sealed class StateManager( $"Reset advanced customization and dye state of {state.Identifier.Incognito(null)} to game base. [Affecting {actors.ToLazyString("nothing")}.]"); StateChanged.Invoke(StateChangeType.Reset, source, state, actors, null); // Update that we have completed a full operation. (We can do this directly as nothing else is linked) - StateUpdated.Invoke(StateFinalizationType.RevertAdvanced, actors); + StateFinalized.Invoke(StateFinalizationType.RevertAdvanced, actors); } public void ResetCustomize(ActorState state, StateSource source, uint key = 0) @@ -325,7 +325,7 @@ public sealed class StateManager( Glamourer.Log.Verbose( $"Reset customization state of {state.Identifier.Incognito(null)} to game base. [Affecting {actors.ToLazyString("nothing")}.]"); // Update that we have completed a full operation. (We can do this directly as nothing else is linked) - StateUpdated.Invoke(StateFinalizationType.RevertCustomize, actors); + StateFinalized.Invoke(StateFinalizationType.RevertCustomize, actors); } public void ResetEquip(ActorState state, StateSource source, uint key = 0) @@ -376,7 +376,7 @@ public sealed class StateManager( Glamourer.Log.Verbose( $"Reset equipment state of {state.Identifier.Incognito(null)} to game base. [Affecting {actors.ToLazyString("nothing")}.]"); // Update that we have completed a full operation. (We can do this directly as nothing else is linked) - StateUpdated.Invoke(StateFinalizationType.RevertEquipment, actors); + StateFinalized.Invoke(StateFinalizationType.RevertEquipment, actors); } public void ResetStateFixed(ActorState state, bool respectManualPalettes, uint key = 0) @@ -453,23 +453,23 @@ public sealed class StateManager( } } - public void ReapplyState(Actor actor, bool forceRedraw, StateSource source, bool stateUpdate = false) + public void ReapplyState(Actor actor, bool forceRedraw, StateSource source, bool isFinal = false) { if (!GetOrCreate(actor, out var state)) return; - ReapplyState(actor, state, forceRedraw, source, stateUpdate); + ReapplyState(actor, state, forceRedraw, source, isFinal); } - public void ReapplyState(Actor actor, ActorState state, bool forceRedraw, StateSource source, bool stateUpdate) + public void ReapplyState(Actor actor, ActorState state, bool forceRedraw, StateSource source, bool isFinal) { var data = Applier.ApplyAll(state, forceRedraw || !actor.Model.IsHuman || CustomizeArray.Compare(actor.Model.GetCustomize(), state.ModelData.Customize).RequiresRedraw(), false); StateChanged.Invoke(StateChangeType.Reapply, source, state, data, null); - if(stateUpdate) - StateUpdated.Invoke(StateFinalizationType.Reapply, data); + if(isFinal) + StateFinalized.Invoke(StateFinalizationType.Reapply, data); } /// Automation variant for reapply, to fire the correct StateUpdateType once reapplied. @@ -490,7 +490,7 @@ public sealed class StateManager( || CustomizeArray.Compare(actor.Model.GetCustomize(), state.ModelData.Customize).RequiresRedraw(), false); StateChanged.Invoke(StateChangeType.Reapply, source, state, data, null); // invoke the automation update based on what reset is. - StateUpdated.Invoke(wasReset ? StateFinalizationType.RevertAutomation : StateFinalizationType.ReapplyAutomation, data); + StateFinalized.Invoke(wasReset ? StateFinalizationType.RevertAutomation : StateFinalizationType.ReapplyAutomation, data); } public void DeleteState(ActorIdentifier identifier)