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/DesignsApi.cs b/Glamourer/Api/DesignsApi.cs index ee49bd5..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); + 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 8639a22..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; @@ -52,6 +51,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.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 331942b..eaf9d01 100644 --- a/Glamourer/Api/StateApi.cs +++ b/Glamourer/Api/StateApi.cs @@ -23,6 +23,7 @@ public sealed class StateApi : IGlamourerApiState, IApiService, IDisposable private readonly AutoDesignApplier _autoDesigns; private readonly ObjectManager _objects; private readonly StateChanged _stateChanged; + private readonly StateFinalized _stateFinalized; private readonly GPoseService _gPose; public StateApi(ApiHelpers helpers, @@ -32,23 +33,27 @@ public sealed class StateApi : IGlamourerApiState, IApiService, IDisposable AutoDesignApplier autoDesigns, ObjectManager objects, StateChanged stateChanged, + StateFinalized stateFinalized, GPoseService gPose) { - _helpers = helpers; - _stateManager = stateManager; - _converter = converter; - _config = config; - _autoDesigns = autoDesigns; - _objects = objects; - _stateChanged = stateChanged; - _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); - _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); + _stateFinalized.Unsubscribe(OnStateFinalized); _gPose.Unsubscribe(OnGPoseChange); } @@ -248,15 +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? GPoseChanged; + public event Action? StateChanged; + public event Action? StateChangedWithType; + public event Action? StateFinalized; + 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, IsFinal: 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,6 +339,7 @@ public sealed class StateApi : IGlamourerApiState, IApiService, IDisposable private void OnStateChanged(StateChangeType type, StateSource _2, ActorState _3, ActorData actors, ITransaction? _5) { + 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); @@ -341,4 +348,12 @@ public sealed class StateApi : IGlamourerApiState, IApiService, IDisposable foreach (var actor in actors.Objects) StateChangedWithType.Invoke(actor.Address, type); } + + private void OnStateFinalized(StateFinalizationType type, ActorData actors) + { + Glamourer.Log.Verbose($"[OnStateUpdated] State Updated with Type {type}. [Affecting {actors.ToLazyString("nothing")}.]"); + if (StateFinalized != null) + foreach (var actor in actors.Objects) + StateFinalized.Invoke(actor.Address, type); + } } 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/Designs/IDesignEditor.cs b/Glamourer/Designs/IDesignEditor.cs index 935263b..c18c98b 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 IsFinal = false) { public static readonly ApplySettings Manual = new() { @@ -24,6 +25,7 @@ public readonly record struct ApplySettings( UseSingleSource = false, MergeLinks = false, ResetMaterials = false, + IsFinal = false, }; public static readonly ApplySettings ManualWithLinks = new() @@ -35,6 +37,7 @@ public readonly record struct ApplySettings( UseSingleSource = false, MergeLinks = true, ResetMaterials = false, + IsFinal = false, }; public static readonly ApplySettings Game = new() @@ -46,6 +49,7 @@ public readonly record struct ApplySettings( UseSingleSource = false, MergeLinks = false, ResetMaterials = true, + 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/GearsetDataLoaded.cs b/Glamourer/Events/GearsetDataLoaded.cs new file mode 100644 index 0000000..4750939 --- /dev/null +++ b/Glamourer/Events/GearsetDataLoaded.cs @@ -0,0 +1,21 @@ +using OtterGui.Classes; +using Penumbra.GameData.Interop; + +namespace Glamourer.Events; + +/// +/// 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 draw object associated with the finished load (Also fired by other players on render) +/// +/// +public sealed class GearsetDataLoaded() + : EventWrapper(nameof(GearsetDataLoaded)) +{ + public enum Priority + { + /// + StateListener = 0, + } +} diff --git a/Glamourer/Events/StateFinalized.cs b/Glamourer/Events/StateFinalized.cs new file mode 100644 index 0000000..e8548e9 --- /dev/null +++ b/Glamourer/Events/StateFinalized.cs @@ -0,0 +1,23 @@ +using Glamourer.Api; +using Glamourer.Api.Enums; +using Glamourer.Interop.Structs; +using OtterGui.Classes; + +namespace Glamourer.Events; + +/// +/// 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 StateFinalized() + : EventWrapper(nameof(StateFinalized)) +{ + public enum Priority + { + /// + StateApi = int.MinValue, + } +} diff --git a/Glamourer/Gui/DesignQuickBar.cs b/Glamourer/Gui/DesignQuickBar.cs index 31ca45e..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); + _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); + _stateManager.ResetState(state!, StateSource.Manual, isFinal: 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..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); + _stateManager.ResetState(_state!, StateSource.Manual, isFinal: 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 { 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); + 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); + 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 394bd7f..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); + _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 070ca1e..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); + _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); + _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 312bceb..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); + _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); + _state.ApplyDesign(state, design, ApplySettings.Manual with { IsFinal = true }); } } 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 d1004e6..ffa6581 100644 --- a/Glamourer/Interop/UpdateSlotService.cs +++ b/Glamourer/Interop/UpdateSlotService.cs @@ -1,6 +1,8 @@ using Dalamud.Hooking; 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; @@ -14,23 +16,29 @@ public unsafe class UpdateSlotService : IDisposable { public readonly EquipSlotUpdating EquipSlotUpdatingEvent; public readonly BonusSlotUpdating BonusSlotUpdatingEvent; - private readonly DictBonusItems _bonusItems; + public readonly GearsetDataLoaded GearsetDataLoadedEvent; + private readonly DictBonusItems _bonusItems; - 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; - _bonusItems = bonusItems; + GearsetDataLoadedEvent = gearsetDataLoaded; + _bonusItems = bonusItems; + interop.InitializeFromAttributes(this); + _loadGearsetDataHook = interop.HookFromAddress((nint)DrawDataContainer.MemberFunctionPointers.LoadGearsetData, LoadGearsetDataDetour); _flagSlotForUpdateHook.Enable(); _flagBonusSlotForUpdateHook.Enable(); + _loadGearsetDataHook.Enable(); } public void Dispose() { _flagSlotForUpdateHook.Dispose(); _flagBonusSlotForUpdateHook.Dispose(); + _loadGearsetDataHook.Dispose(); } public void UpdateEquipSlot(Model drawObject, EquipSlot slot, CharacterArmor data) @@ -79,6 +87,12 @@ public unsafe class UpdateSlotService : IDisposable [Signature(Sigs.FlagBonusSlotForUpdate, DetourName = nameof(FlagBonusSlotForUpdateDetour))] private readonly Hook _flagBonusSlotForUpdateHook = null!; + /// 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 ulong LoadGearsetDataDelegate(DrawDataContainer* drawDataContainer, PacketPlayerGearsetData* gearsetData); + private readonly Hook _loadGearsetDataHook; + private ulong FlagSlotForUpdateDetour(nint drawObject, uint slotIdx, CharacterArmor* data) { var slot = slotIdx.ToEquipSlot(); @@ -98,5 +112,35 @@ public unsafe class UpdateSlotService : IDisposable } private ulong FlagSlotForUpdateInterop(Model drawObject, EquipSlot slot, CharacterArmor armor) - => _flagSlotForUpdateHook.Original(drawObject.Address, slot.ToIndex(), &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 ulong LoadGearsetDataDetour(DrawDataContainer* drawDataContainer, PacketPlayerGearsetData* gearsetData) + { + var ret = _loadGearsetDataHook.Original(drawDataContainer, gearsetData); + var drawObject = drawDataContainer->OwnerObject->DrawObject; + GearsetDataLoadedEvent.Invoke(drawObject); + Glamourer.Log.Excessive($"[LoadAllEquipmentDetour] GearsetItemData: {FormatGearsetItemDataStruct(*gearsetData)}"); + return ret; + } + + + private static string FormatGearsetItemDataStruct(PacketPlayerGearsetData gearsetData) + { + 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 (var offset = 20; offset <= 56; offset += sizeof(LegacyCharacterArmor)) + { + 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 10f68ee..98dfa19 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 { 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); + _stateManager.ApplyDesign(state, design, ApplySettings.ManualWithLinks with { IsFinal = true }); } } } diff --git a/Glamourer/State/StateEditor.cs b/Glamourer/State/StateEditor.cs index f9ddb89..42058d2 100644 --- a/Glamourer/State/StateEditor.cs +++ b/Glamourer/State/StateEditor.cs @@ -17,6 +17,7 @@ public class StateEditor( InternalStateEditor editor, StateApplier applier, StateChanged stateChanged, + StateFinalized stateFinalized, JobChangeState jobChange, Configuration config, ItemManager items, @@ -24,11 +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 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, @@ -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); + StateFinalized.Invoke(StateFinalizationType.ModelChange, actors); } /// @@ -380,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) @@ -417,6 +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.IsFinal) + StateFinalized.Invoke(StateFinalizationType.DesignApplied, actors); return; @@ -437,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 @@ -455,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 651e63e..3a6d6ef 100644 --- a/Glamourer/State/StateListener.cs +++ b/Glamourer/State/StateListener.cs @@ -14,6 +14,7 @@ using Glamourer.GameData; using Penumbra.GameData.DataContainers; using Glamourer.Designs; using Penumbra.GameData.Interop; +using Glamourer.Api.Enums; using ObjectManager = Glamourer.Interop.ObjectManager; namespace Glamourer.State; @@ -35,10 +36,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 StateFinalized _stateFinalized; private readonly AutoDesignApplier _autoDesignApplier; private readonly FunModule _funModule; private readonly HumanModelList _humans; @@ -56,11 +59,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, StateFinalized stateFinalized) { _manager = manager; _items = items; @@ -68,6 +71,7 @@ public class StateListener : IDisposable _actors = actors; _config = config; _equipSlotUpdating = equipSlotUpdating; + _gearsetDataLoaded = gearsetDataLoaded; _weaponLoading = weaponLoading; _visorState = visorState; _weaponVisibility = weaponVisibility; @@ -84,6 +88,7 @@ public class StateListener : IDisposable _condition = condition; _crestService = crestService; _bonusSlotUpdating = bonusSlotUpdating; + _stateFinalized = stateFinalized; Subscribe(); } @@ -220,7 +225,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) + if (actor.Identifier(_actors, out var identifier) && _manager.TryGetValue(identifier, out var state)) { HandleEquipSlot(actor, state, slot, ref armor); @@ -264,6 +269,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) + _stateFinalized.Invoke(StateFinalizationType.Gearset, actors); + } + + private void OnMovedEquipment((EquipSlot, uint, StainIds)[] items) { _objects.Update(); @@ -787,6 +808,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); @@ -804,6 +826,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 eabaf2f..9b71586 100644 --- a/Glamourer/State/StateManager.cs +++ b/Glamourer/State/StateManager.cs @@ -18,19 +18,20 @@ using Penumbra.GameData.Interop; namespace Glamourer.State; public sealed class StateManager( - ActorManager _actors, + ActorManager actors, ItemManager items, - StateChanged @event, + 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, jobChange, config, items, merger, modApplier, gPose), + : StateEditor(editor, applier, changeEvent, finalizeEvent, jobChange, config, items, merger, modApplier, gPose), IReadOnlyDictionary { private readonly Dictionary _states = []; @@ -61,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) @@ -81,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); @@ -114,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])); @@ -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 isFinal = false) { if (!state.Unlock(key)) return; @@ -276,6 +277,9 @@ public sealed class StateManager( 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. + if(isFinal) + StateFinalized.Invoke(StateFinalizationType.Revert, actors); } public void ResetAdvancedState(ActorState state, StateSource source, uint key = 0) @@ -301,6 +305,8 @@ public sealed class StateManager( 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) + StateFinalized.Invoke(StateFinalizationType.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) + StateFinalized.Invoke(StateFinalizationType.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) + StateFinalized.Invoke(StateFinalizationType.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 isFinal = false) { if (!GetOrCreate(actor, out var state)) return; - ReapplyState(actor, state, forceRedraw, source); + ReapplyState(actor, state, forceRedraw, source, isFinal); } - public void ReapplyState(Actor actor, ActorState state, bool forceRedraw, StateSource source) + 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(isFinal) + StateFinalized.Invoke(StateFinalizationType.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. + StateFinalized.Invoke(wasReset ? StateFinalizationType.RevertAutomation : StateFinalizationType.ReapplyAutomation, data); } public void DeleteState(ActorIdentifier identifier)