From e1a41b5f3c7d144cae20c291ff2ee2fe77978e87 Mon Sep 17 00:00:00 2001 From: Cordelia Mist Date: Fri, 17 Jan 2025 17:39:26 -0800 Subject: [PATCH] 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)