Merge branch 'refs/heads/CordeliaMist/main'

This commit is contained in:
Ottermandias 2025-01-24 17:51:11 +01:00
commit 8b518d3c6f
22 changed files with 316 additions and 106 deletions

@ -1 +1 @@
Subproject commit b1b90e6ecfeee76a12cb27793753fa87af21083f Subproject commit 4ac38fbed6fb0f31c0b75de26950ab82d3bee258

View file

@ -33,7 +33,7 @@ public class DesignsApi(ApiHelpers helpers, DesignManager designs, StateManager
{ {
var once = (flags & ApplyFlag.Once) != 0; var once = (flags & ApplyFlag.Once) != 0;
var settings = new ApplySettings(Source: once ? StateSource.IpcManual : StateSource.IpcFixed, Key: key, MergeLinks: true, 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); using var restrict = ApiHelpers.Restrict(design, flags);
stateManager.ApplyDesign(state, design, settings); stateManager.ApplyDesign(state, design, settings);

View file

@ -2,7 +2,6 @@ using Dalamud.Plugin;
using Glamourer.Api.Api; using Glamourer.Api.Api;
using Glamourer.Api.Helpers; using Glamourer.Api.Helpers;
using OtterGui.Services; using OtterGui.Services;
using System.Reflection.Emit;
using Glamourer.Api.Enums; using Glamourer.Api.Enums;
namespace Glamourer.Api; namespace Glamourer.Api;
@ -52,6 +51,7 @@ public sealed class IpcProviders : IDisposable, IApiService
IpcSubscribers.RevertToAutomationName.Provider(pi, api.State), IpcSubscribers.RevertToAutomationName.Provider(pi, api.State),
IpcSubscribers.StateChanged.Provider(pi, api.State), IpcSubscribers.StateChanged.Provider(pi, api.State),
IpcSubscribers.StateChangedWithType.Provider(pi, api.State), IpcSubscribers.StateChangedWithType.Provider(pi, api.State),
IpcSubscribers.StateFinalized.Provider(pi, api.State),
IpcSubscribers.GPoseChanged.Provider(pi, api.State), IpcSubscribers.GPoseChanged.Provider(pi, api.State),
]; ];
_initializedProvider.Invoke(); _initializedProvider.Invoke();

View file

@ -23,6 +23,7 @@ public sealed class StateApi : IGlamourerApiState, IApiService, IDisposable
private readonly AutoDesignApplier _autoDesigns; private readonly AutoDesignApplier _autoDesigns;
private readonly ObjectManager _objects; private readonly ObjectManager _objects;
private readonly StateChanged _stateChanged; private readonly StateChanged _stateChanged;
private readonly StateFinalized _stateFinalized;
private readonly GPoseService _gPose; private readonly GPoseService _gPose;
public StateApi(ApiHelpers helpers, public StateApi(ApiHelpers helpers,
@ -32,23 +33,27 @@ public sealed class StateApi : IGlamourerApiState, IApiService, IDisposable
AutoDesignApplier autoDesigns, AutoDesignApplier autoDesigns,
ObjectManager objects, ObjectManager objects,
StateChanged stateChanged, StateChanged stateChanged,
StateFinalized stateFinalized,
GPoseService gPose) GPoseService gPose)
{ {
_helpers = helpers; _helpers = helpers;
_stateManager = stateManager; _stateManager = stateManager;
_converter = converter; _converter = converter;
_config = config; _config = config;
_autoDesigns = autoDesigns; _autoDesigns = autoDesigns;
_objects = objects; _objects = objects;
_stateChanged = stateChanged; _stateChanged = stateChanged;
_gPose = gPose; _stateFinalized = stateFinalized;
_gPose = gPose;
_stateChanged.Subscribe(OnStateChanged, Events.StateChanged.Priority.GlamourerIpc); _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() public void Dispose()
{ {
_stateChanged.Unsubscribe(OnStateChanged); _stateChanged.Unsubscribe(OnStateChanged);
_stateFinalized.Unsubscribe(OnStateFinalized);
_gPose.Unsubscribe(OnGPoseChange); _gPose.Unsubscribe(OnGPoseChange);
} }
@ -248,15 +253,16 @@ public sealed class StateApi : IGlamourerApiState, IApiService, IDisposable
return ApiHelpers.Return(GlamourerApiEc.Success, args); return ApiHelpers.Return(GlamourerApiEc.Success, args);
} }
public event Action<nint>? StateChanged; public event Action<nint>? StateChanged;
public event Action<IntPtr, StateChangeType>? StateChangedWithType; public event Action<IntPtr, StateChangeType>? StateChangedWithType;
public event Action<bool>? GPoseChanged; public event Action<IntPtr, StateFinalizationType>? StateFinalized;
public event Action<bool>? GPoseChanged;
private void ApplyDesign(ActorState state, DesignBase design, uint key, ApplyFlag flags) private void ApplyDesign(ActorState state, DesignBase design, uint key, ApplyFlag flags)
{ {
var once = (flags & ApplyFlag.Once) != 0; var once = (flags & ApplyFlag.Once) != 0;
var settings = new ApplySettings(Source: once ? StateSource.IpcManual : StateSource.IpcFixed, Key: key, MergeLinks: true, 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); _stateManager.ApplyDesign(state, design, settings);
ApiHelpers.Lock(state, key, flags); 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; var source = (flags & ApplyFlag.Once) != 0 ? StateSource.IpcManual : StateSource.IpcFixed;
_autoDesigns.ReapplyAutomation(actor, state.Identifier, state, true, out var forcedRedraw); _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); 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) 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) if (StateChanged != null)
foreach (var actor in actors.Objects) foreach (var actor in actors.Objects)
StateChanged.Invoke(actor.Address); StateChanged.Invoke(actor.Address);
@ -341,4 +348,12 @@ public sealed class StateApi : IGlamourerApiState, IApiService, IDisposable
foreach (var actor in actors.Objects) foreach (var actor in actors.Objects)
StateChangedWithType.Invoke(actor.Address, type); 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);
}
} }

View file

@ -163,7 +163,7 @@ public sealed class AutoDesignApplier : IDisposable
{ {
Reduce(data.Objects[0], state, newSet, _config.RespectManualOnAutomationUpdate, false, true, out var forcedRedraw); Reduce(data.Objects[0], state, newSet, _config.RespectManualOnAutomationUpdate, false, true, out var forcedRedraw);
foreach (var actor in data.Objects) 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)) 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)) if (_state.GetOrCreate(specificId, actor, out var state))
{ {
Reduce(actor, state, newSet, _config.RespectManualOnAutomationUpdate, false, true, out var forcedRedraw); Reduce(actor, state, newSet, _config.RespectManualOnAutomationUpdate, false, true, out var forcedRedraw);
_state.ReapplyState(actor, forcedRedraw, StateSource.Fixed); _state.ReapplyAutomationState(actor, forcedRedraw, false, StateSource.Fixed);
} }
} }
} }

View file

@ -13,7 +13,8 @@ public readonly record struct ApplySettings(
bool FromJobChange = false, bool FromJobChange = false,
bool UseSingleSource = false, bool UseSingleSource = false,
bool MergeLinks = false, bool MergeLinks = false,
bool ResetMaterials = false) bool ResetMaterials = false,
bool IsFinal = false)
{ {
public static readonly ApplySettings Manual = new() public static readonly ApplySettings Manual = new()
{ {
@ -24,6 +25,7 @@ public readonly record struct ApplySettings(
UseSingleSource = false, UseSingleSource = false,
MergeLinks = false, MergeLinks = false,
ResetMaterials = false, ResetMaterials = false,
IsFinal = false,
}; };
public static readonly ApplySettings ManualWithLinks = new() public static readonly ApplySettings ManualWithLinks = new()
@ -35,6 +37,7 @@ public readonly record struct ApplySettings(
UseSingleSource = false, UseSingleSource = false,
MergeLinks = true, MergeLinks = true,
ResetMaterials = false, ResetMaterials = false,
IsFinal = false,
}; };
public static readonly ApplySettings Game = new() public static readonly ApplySettings Game = new()
@ -46,6 +49,7 @@ public readonly record struct ApplySettings(
UseSingleSource = false, UseSingleSource = false,
MergeLinks = false, MergeLinks = false,
ResetMaterials = true, ResetMaterials = true,
IsFinal = false,
}; };
} }

View file

@ -13,8 +13,8 @@ public sealed class GPoseService : EventWrapper<bool, GPoseService.Priority>
public enum Priority public enum Priority
{ {
/// <seealso cref="Api.GlamourerIpc.OnGPoseChanged"/> /// <seealso cref="Api.StateApi.OnGPoseChange"/>
GlamourerIpc = int.MinValue, StateApi = int.MinValue,
} }
public bool InGPose { get; private set; } public bool InGPose { get; private set; }

View file

@ -0,0 +1,21 @@
using OtterGui.Classes;
using Penumbra.GameData.Interop;
namespace Glamourer.Events;
/// <summary>
/// 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.
/// <list type="number">
/// <item>The model draw object associated with the finished load (Also fired by other players on render) </item>
/// </list>
/// </summary>
public sealed class GearsetDataLoaded()
: EventWrapper<Model, GearsetDataLoaded.Priority>(nameof(GearsetDataLoaded))
{
public enum Priority
{
/// <seealso cref="State.StateListener.OnGearsetDataLoaded"/>
StateListener = 0,
}
}

View file

@ -0,0 +1,23 @@
using Glamourer.Api;
using Glamourer.Api.Enums;
using Glamourer.Interop.Structs;
using OtterGui.Classes;
namespace Glamourer.Events;
/// <summary>
/// Triggered when a set of grouped changes finishes being applied to a Glamourer state.
/// <list type="number">
/// <item>Parameter is the operation that finished updating the saved state. </item>
/// <item>Parameter is the existing actors using this saved state. </item>
/// </list>
/// </summary>
public sealed class StateFinalized()
: EventWrapper<StateFinalizationType, ActorData, StateFinalized.Priority>(nameof(StateFinalized))
{
public enum Priority
{
/// <seealso cref="StateApi.OnStateFinalized"/>
StateApi = int.MinValue,
}
}

View file

@ -183,7 +183,7 @@ public sealed class DesignQuickBar : Window, IDisposable
} }
using var _ = design!.TemporarilyRestrictApplication(ApplicationCollection.FromKeys()); 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) private void DrawRevertButton(Vector2 buttonSize)
@ -213,7 +213,7 @@ public sealed class DesignQuickBar : Window, IDisposable
var (clicked, _, _, state) = ResolveTarget(FontAwesomeIcon.UndoAlt, buttonSize, tooltip, available); var (clicked, _, _, state) = ResolveTarget(FontAwesomeIcon.UndoAlt, buttonSize, tooltip, available);
ImGui.SameLine(); ImGui.SameLine();
if (clicked) if (clicked)
_stateManager.ResetState(state!, StateSource.Manual); _stateManager.ResetState(state!, StateSource.Manual, isFinal: true);
} }
private void DrawRevertAutomationButton(Vector2 buttonSize) private void DrawRevertAutomationButton(Vector2 buttonSize)
@ -252,7 +252,7 @@ public sealed class DesignQuickBar : Window, IDisposable
foreach (var actor in data.Objects) foreach (var actor in data.Objects)
{ {
_autoDesignApplier.ReapplyAutomation(actor, id, state!, true, out var forcedRedraw); _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) foreach (var actor in data.Objects)
{ {
_autoDesignApplier.ReapplyAutomation(actor, id, state!, false, out var forcedRedraw); _autoDesignApplier.ReapplyAutomation(actor, id, state!, false, out var forcedRedraw);
_stateManager.ReapplyState(actor, forcedRedraw, StateSource.Manual); _stateManager.ReapplyAutomationState(actor, forcedRedraw, false, StateSource.Manual);
} }
} }

View file

@ -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.", if (ImGuiUtil.DrawDisabledButton("Revert to Game", Vector2.Zero, "Revert the character to its actual state in the game.",
_state!.IsLocked)) _state!.IsLocked))
_stateManager.ResetState(_state!, StateSource.Manual); _stateManager.ResetState(_state!, StateSource.Manual, isFinal: true);
ImGui.SameLine(); ImGui.SameLine();
@ -394,7 +394,7 @@ public class ActorPanel
!_config.EnableAutoDesigns || _state!.IsLocked)) !_config.EnableAutoDesigns || _state!.IsLocked))
{ {
_autoDesignApplier.ReapplyAutomation(_actor, _identifier, _state!, false, out var forcedRedraw); _autoDesignApplier.ReapplyAutomation(_actor, _identifier, _state!, false, out var forcedRedraw);
_stateManager.ReapplyState(_actor, forcedRedraw, StateSource.Manual); _stateManager.ReapplyAutomationState(_actor, forcedRedraw, false, StateSource.Manual);
} }
ImGui.SameLine(); ImGui.SameLine();
@ -403,14 +403,14 @@ public class ActorPanel
!_config.EnableAutoDesigns || _state!.IsLocked)) !_config.EnableAutoDesigns || _state!.IsLocked))
{ {
_autoDesignApplier.ReapplyAutomation(_actor, _identifier, _state!, true, out var forcedRedraw); _autoDesignApplier.ReapplyAutomation(_actor, _identifier, _state!, true, out var forcedRedraw);
_stateManager.ReapplyState(_actor, forcedRedraw, StateSource.Manual); _stateManager.ReapplyAutomationState(_actor, forcedRedraw, true, StateSource.Manual);
} }
ImGui.SameLine(); ImGui.SameLine();
if (ImGuiUtil.DrawDisabledButton("Reapply", Vector2.Zero, if (ImGuiUtil.DrawDisabledButton("Reapply", Vector2.Zero,
"Try to reapply the configured state if something went wrong. Should generally not be necessary.", "Try to reapply the configured state if something went wrong. Should generally not be necessary.",
_state!.IsLocked)) _state!.IsLocked))
_stateManager.ReapplyState(_actor, false, StateSource.Manual); _stateManager.ReapplyState(_actor, false, StateSource.Manual, true);
} }
private void DrawApplyToSelf() private void DrawApplyToSelf()
@ -423,7 +423,7 @@ public class ActorPanel
if (_stateManager.GetOrCreate(id, data.Objects[0], out var state)) if (_stateManager.GetOrCreate(id, data.Objects[0], out var state))
_stateManager.ApplyDesign(state, _converter.Convert(_state!, ApplicationRules.FromModifiers(_state!)), _stateManager.ApplyDesign(state, _converter.Convert(_state!, ApplicationRules.FromModifiers(_state!)),
ApplySettings.Manual); ApplySettings.Manual with { IsFinal = true });
} }
private void DrawApplyToTarget() private void DrawApplyToTarget()
@ -440,7 +440,7 @@ public class ActorPanel
if (_stateManager.GetOrCreate(id, data.Objects[0], out var state)) if (_stateManager.GetOrCreate(id, data.Objects[0], out var state))
_stateManager.ApplyDesign(state, _converter.Convert(_state!, ApplicationRules.FromModifiers(_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 text = ImGui.GetClipboardText();
var design = panel._converter.FromBase64(text, applyCustomize, applyGear, out _) var design = panel._converter.FromBase64(text, applyCustomize, applyGear, out _)
?? throw new Exception("The clipboard did not contain valid data."); ?? 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) catch (Exception ex)
{ {

View file

@ -79,7 +79,7 @@ public unsafe class GlamourPlatePanel : IGameDataDrawer
if (ImGuiUtil.DrawDisabledButton("Apply to Player", Vector2.Zero, string.Empty, !enabled)) if (ImGuiUtil.DrawDisabledButton("Apply to Player", Vector2.Zero, string.Empty, !enabled))
{ {
var design = CreateDesign(plate); var design = CreateDesign(plate);
_state.ApplyDesign(state!, design, ApplySettings.Manual); _state.ApplyDesign(state!, design, ApplySettings.Manual with { IsFinal = true });
} }
using (ImRaii.Group()) using (ImRaii.Group())

View file

@ -51,6 +51,7 @@ public class IpcTesterPanel(
Glamourer.Log.Debug("[IPCTester] Subscribed to IPC events for IPC tester."); Glamourer.Log.Debug("[IPCTester] Subscribed to IPC events for IPC tester.");
state.GPoseChanged.Enable(); state.GPoseChanged.Enable();
state.StateChanged.Enable(); state.StateChanged.Enable();
state.StateFinalized.Enable();
framework.Update += CheckUnsubscribe; framework.Update += CheckUnsubscribe;
_subscribed = true; _subscribed = true;
} }
@ -73,5 +74,6 @@ public class IpcTesterPanel(
_subscribed = false; _subscribed = false;
state.GPoseChanged.Disable(); state.GPoseChanged.Disable();
state.StateChanged.Disable(); state.StateChanged.Disable();
state.StateFinalized.Disable();
} }
} }

View file

@ -11,6 +11,7 @@ using Newtonsoft.Json.Linq;
using OtterGui; using OtterGui;
using OtterGui.Raii; using OtterGui.Raii;
using OtterGui.Services; using OtterGui.Services;
using OtterGui.Text;
using Penumbra.GameData.Interop; using Penumbra.GameData.Interop;
using Penumbra.String; using Penumbra.String;
@ -31,9 +32,16 @@ public class StateIpcTester : IUiService, IDisposable
private string? _getStateString; private string? _getStateString;
public readonly EventSubscriber<nint, StateChangeType> StateChanged; public readonly EventSubscriber<nint, StateChangeType> StateChanged;
private nint _lastStateChangeActor; private nint _lastStateChangeActor;
private ByteString _lastStateChangeName = ByteString.Empty; private ByteString _lastStateChangeName = ByteString.Empty;
private DateTime _lastStateChangeTime; private DateTime _lastStateChangeTime;
private StateChangeType _lastStateChangeType;
public readonly EventSubscriber<nint, StateFinalizationType> StateFinalized;
private nint _lastStateFinalizeActor;
private ByteString _lastStateFinalizeName = ByteString.Empty;
private DateTime _lastStateFinalizeTime;
private StateFinalizationType _lastStateFinalizeType;
public readonly EventSubscriber<bool> GPoseChanged; public readonly EventSubscriber<bool> GPoseChanged;
private bool _lastGPoseChangeValue; private bool _lastGPoseChangeValue;
@ -44,15 +52,18 @@ public class StateIpcTester : IUiService, IDisposable
public StateIpcTester(IDalamudPluginInterface pluginInterface) public StateIpcTester(IDalamudPluginInterface pluginInterface)
{ {
_pluginInterface = 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); GPoseChanged = Api.IpcSubscribers.GPoseChanged.Subscriber(_pluginInterface, OnGPoseChange);
StateChanged.Disable(); StateChanged.Disable();
StateFinalized.Disable();
GPoseChanged.Disable(); GPoseChanged.Disable();
} }
public void Dispose() public void Dispose()
{ {
StateChanged.Dispose(); StateChanged.Dispose();
StateFinalized.Dispose();
GPoseChanged.Dispose(); GPoseChanged.Dispose();
} }
@ -73,86 +84,88 @@ public class StateIpcTester : IUiService, IDisposable
IpcTesterHelpers.DrawIntro("Last Error"); IpcTesterHelpers.DrawIntro("Last Error");
ImGui.TextUnformatted(_lastError.ToString()); ImGui.TextUnformatted(_lastError.ToString());
IpcTesterHelpers.DrawIntro("Last State Change"); IpcTesterHelpers.DrawIntro("Last State Change");
PrintName(); PrintChangeName();
IpcTesterHelpers.DrawIntro("Last State Finalization");
PrintFinalizeName();
IpcTesterHelpers.DrawIntro("Last GPose Change"); IpcTesterHelpers.DrawIntro("Last GPose Change");
ImGui.TextUnformatted($"{_lastGPoseChangeValue} at {_lastGPoseChangeTime.ToLocalTime().TimeOfDay}"); ImGui.TextUnformatted($"{_lastGPoseChangeValue} at {_lastGPoseChangeTime.ToLocalTime().TimeOfDay}");
IpcTesterHelpers.DrawIntro(GetState.Label); IpcTesterHelpers.DrawIntro(GetState.Label);
DrawStatePopup(); DrawStatePopup();
if (ImGui.Button("Get##Idx")) if (ImUtf8.Button("Get##Idx"u8))
{ {
(_lastError, _state) = new GetState(_pluginInterface).Invoke(_gameObjectIndex, _key); (_lastError, _state) = new GetState(_pluginInterface).Invoke(_gameObjectIndex, _key);
_stateString = _state?.ToString(Formatting.Indented) ?? "No State Available"; _stateString = _state?.ToString(Formatting.Indented) ?? "No State Available";
ImGui.OpenPopup("State"); ImUtf8.OpenPopup("State"u8);
} }
IpcTesterHelpers.DrawIntro(GetStateName.Label); IpcTesterHelpers.DrawIntro(GetStateName.Label);
if (ImGui.Button("Get##Name")) if (ImUtf8.Button("Get##Name"u8))
{ {
(_lastError, _state) = new GetStateName(_pluginInterface).Invoke(_gameObjectName, _key); (_lastError, _state) = new GetStateName(_pluginInterface).Invoke(_gameObjectName, _key);
_stateString = _state?.ToString(Formatting.Indented) ?? "No State Available"; _stateString = _state?.ToString(Formatting.Indented) ?? "No State Available";
ImGui.OpenPopup("State"); ImUtf8.OpenPopup("State"u8);
} }
IpcTesterHelpers.DrawIntro(GetStateBase64.Label); IpcTesterHelpers.DrawIntro(GetStateBase64.Label);
if (ImGui.Button("Get##Base64Idx")) if (ImUtf8.Button("Get##Base64Idx"u8))
{ {
(_lastError, _getStateString) = new GetStateBase64(_pluginInterface).Invoke(_gameObjectIndex, _key); (_lastError, _getStateString) = new GetStateBase64(_pluginInterface).Invoke(_gameObjectIndex, _key);
_stateString = _getStateString ?? "No State Available"; _stateString = _getStateString ?? "No State Available";
ImGui.OpenPopup("State"); ImUtf8.OpenPopup("State"u8);
} }
IpcTesterHelpers.DrawIntro(GetStateBase64Name.Label); IpcTesterHelpers.DrawIntro(GetStateBase64Name.Label);
if (ImGui.Button("Get##Base64Idx")) if (ImUtf8.Button("Get##Base64Idx"u8))
{ {
(_lastError, _getStateString) = new GetStateBase64Name(_pluginInterface).Invoke(_gameObjectName, _key); (_lastError, _getStateString) = new GetStateBase64Name(_pluginInterface).Invoke(_gameObjectName, _key);
_stateString = _getStateString ?? "No State Available"; _stateString = _getStateString ?? "No State Available";
ImGui.OpenPopup("State"); ImUtf8.OpenPopup("State"u8);
} }
IpcTesterHelpers.DrawIntro(ApplyState.Label); IpcTesterHelpers.DrawIntro(ApplyState.Label);
if (ImGuiUtil.DrawDisabledButton("Apply Last##Idx", Vector2.Zero, string.Empty, _state == null)) if (ImGuiUtil.DrawDisabledButton("Apply Last##Idx", Vector2.Zero, string.Empty, _state == null))
_lastError = new ApplyState(_pluginInterface).Invoke(_state!, _gameObjectIndex, _key, _flags); _lastError = new ApplyState(_pluginInterface).Invoke(_state!, _gameObjectIndex, _key, _flags);
ImGui.SameLine(); ImGui.SameLine();
if (ImGui.Button("Apply Base64##Idx")) if (ImUtf8.Button("Apply Base64##Idx"u8))
_lastError = new ApplyState(_pluginInterface).Invoke(_base64State, _gameObjectIndex, _key, _flags); _lastError = new ApplyState(_pluginInterface).Invoke(_base64State, _gameObjectIndex, _key, _flags);
IpcTesterHelpers.DrawIntro(ApplyStateName.Label); IpcTesterHelpers.DrawIntro(ApplyStateName.Label);
if (ImGuiUtil.DrawDisabledButton("Apply Last##Name", Vector2.Zero, string.Empty, _state == null)) if (ImGuiUtil.DrawDisabledButton("Apply Last##Name", Vector2.Zero, string.Empty, _state == null))
_lastError = new ApplyStateName(_pluginInterface).Invoke(_state!, _gameObjectName, _key, _flags); _lastError = new ApplyStateName(_pluginInterface).Invoke(_state!, _gameObjectName, _key, _flags);
ImGui.SameLine(); ImGui.SameLine();
if (ImGui.Button("Apply Base64##Name")) if (ImUtf8.Button("Apply Base64##Name"u8))
_lastError = new ApplyStateName(_pluginInterface).Invoke(_base64State, _gameObjectName, _key, _flags); _lastError = new ApplyStateName(_pluginInterface).Invoke(_base64State, _gameObjectName, _key, _flags);
IpcTesterHelpers.DrawIntro(RevertState.Label); IpcTesterHelpers.DrawIntro(RevertState.Label);
if (ImGui.Button("Revert##Idx")) if (ImUtf8.Button("Revert##Idx"u8))
_lastError = new RevertState(_pluginInterface).Invoke(_gameObjectIndex, _key, _flags); _lastError = new RevertState(_pluginInterface).Invoke(_gameObjectIndex, _key, _flags);
IpcTesterHelpers.DrawIntro(RevertStateName.Label); IpcTesterHelpers.DrawIntro(RevertStateName.Label);
if (ImGui.Button("Revert##Name")) if (ImUtf8.Button("Revert##Name"u8))
_lastError = new RevertStateName(_pluginInterface).Invoke(_gameObjectName, _key, _flags); _lastError = new RevertStateName(_pluginInterface).Invoke(_gameObjectName, _key, _flags);
IpcTesterHelpers.DrawIntro(UnlockState.Label); IpcTesterHelpers.DrawIntro(UnlockState.Label);
if (ImGui.Button("Unlock##Idx")) if (ImUtf8.Button("Unlock##Idx"u8))
_lastError = new UnlockState(_pluginInterface).Invoke(_gameObjectIndex, _key); _lastError = new UnlockState(_pluginInterface).Invoke(_gameObjectIndex, _key);
IpcTesterHelpers.DrawIntro(UnlockStateName.Label); IpcTesterHelpers.DrawIntro(UnlockStateName.Label);
if (ImGui.Button("Unlock##Name")) if (ImUtf8.Button("Unlock##Name"u8))
_lastError = new UnlockStateName(_pluginInterface).Invoke(_gameObjectName, _key); _lastError = new UnlockStateName(_pluginInterface).Invoke(_gameObjectName, _key);
IpcTesterHelpers.DrawIntro(UnlockAll.Label); IpcTesterHelpers.DrawIntro(UnlockAll.Label);
if (ImGui.Button("Unlock##All")) if (ImUtf8.Button("Unlock##All"u8))
_numUnlocked = new UnlockAll(_pluginInterface).Invoke(_key); _numUnlocked = new UnlockAll(_pluginInterface).Invoke(_key);
ImGui.SameLine(); ImGui.SameLine();
ImGui.TextUnformatted($"Unlocked {_numUnlocked}"); ImGui.TextUnformatted($"Unlocked {_numUnlocked}");
IpcTesterHelpers.DrawIntro(RevertToAutomation.Label); IpcTesterHelpers.DrawIntro(RevertToAutomation.Label);
if (ImGui.Button("Revert##AutomationIdx")) if (ImUtf8.Button("Revert##AutomationIdx"u8))
_lastError = new RevertToAutomation(_pluginInterface).Invoke(_gameObjectIndex, _key, _flags); _lastError = new RevertToAutomation(_pluginInterface).Invoke(_gameObjectIndex, _key, _flags);
IpcTesterHelpers.DrawIntro(RevertToAutomationName.Label); IpcTesterHelpers.DrawIntro(RevertToAutomationName.Label);
if (ImGui.Button("Revert##AutomationName")) if (ImUtf8.Button("Revert##AutomationName"u8))
_lastError = new RevertToAutomationName(_pluginInterface).Invoke(_gameObjectName, _key, _flags); _lastError = new RevertToAutomationName(_pluginInterface).Invoke(_gameObjectName, _key, _flags);
} }
@ -162,44 +175,70 @@ public class StateIpcTester : IUiService, IDisposable
if (_stateString == null) if (_stateString == null)
return; return;
using var p = ImRaii.Popup("State"); using var p = ImUtf8.Popup("State"u8);
if (!p) if (!p)
return; return;
if (ImGui.Button("Copy to Clipboard")) if (ImUtf8.Button("Copy to Clipboard"u8))
ImGui.SetClipboardText(_stateString); ImUtf8.SetClipboardText(_stateString);
if (_stateString[0] is '{') if (_stateString[0] is '{')
{ {
ImGui.SameLine(); ImGui.SameLine();
if (ImGui.Button("Copy as Base64") && _state != null) if (ImUtf8.Button("Copy as Base64"u8) && _state != null)
ImGui.SetClipboardText(DesignConverter.ToBase64(_state)); ImUtf8.SetClipboardText(DesignConverter.ToBase64(_state));
} }
using var font = ImRaii.PushFont(UiBuilder.MonoFont); 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(); 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(); ImGui.SameLine();
using (ImRaii.PushFont(UiBuilder.MonoFont)) using (ImRaii.PushFont(UiBuilder.MonoFont))
{ {
ImGuiUtil.CopyOnClickSelectable($"0x{_lastStateChangeActor:X}"); ImUtf8.CopyOnClickSelectable($"0x{_lastStateChangeActor:X}");
} }
ImGui.SameLine(); 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; _lastStateChangeActor = actor;
_lastStateChangeTime = DateTime.UtcNow; _lastStateChangeTime = DateTime.UtcNow;
_lastStateChangeName = actor != nint.Zero ? ((Actor)actor).Utf8Name.Clone() : ByteString.Empty; _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) private void OnGPoseChange(bool value)

View file

@ -460,7 +460,7 @@ public class DesignPanel
if (_state.GetOrCreate(id, data.Objects[0], out var state)) if (_state.GetOrCreate(id, data.Objects[0], out var state))
{ {
using var _ = _selector.Selected!.TemporarilyRestrictApplication(ApplicationCollection.FromKeys()); 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)) if (_state.GetOrCreate(id, data.Objects[0], out var state))
{ {
using var _ = _selector.Selected!.TemporarilyRestrictApplication(ApplicationCollection.FromKeys()); using var _ = _selector.Selected!.TemporarilyRestrictApplication(ApplicationCollection.FromKeys());
_state.ApplyDesign(state, _selector.Selected!, ApplySettings.ManualWithLinks); _state.ApplyDesign(state, _selector.Selected!, ApplySettings.ManualWithLinks with { IsFinal = true });
} }
} }

View file

@ -196,7 +196,7 @@ public class NpcPanel
if (_state.GetOrCreate(id, data.Objects[0], out var state)) if (_state.GetOrCreate(id, data.Objects[0], out var state))
{ {
var design = _converter.Convert(ToDesignData(), new StateMaterialManager(), ApplicationRules.NpcFromModifiers()); 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)) if (_state.GetOrCreate(id, data.Objects[0], out var state))
{ {
var design = _converter.Convert(ToDesignData(), new StateMaterialManager(), ApplicationRules.NpcFromModifiers()); var design = _converter.Convert(ToDesignData(), new StateMaterialManager(), ApplicationRules.NpcFromModifiers());
_state.ApplyDesign(state, design, ApplySettings.Manual); _state.ApplyDesign(state, design, ApplySettings.Manual with { IsFinal = true });
} }
} }

View file

@ -88,7 +88,7 @@ public class PenumbraAutoRedraw : IDisposable, IRequiredService
_actions.Enqueue((state, () => _actions.Enqueue((state, () =>
{ {
foreach (var actor in actors.Objects) 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)}."); Glamourer.Log.Debug($"Automatically applied mod settings of type {type} to {id.Incognito(null)}.");
}, WaitFrames)); }, WaitFrames));
} }
@ -108,7 +108,7 @@ public class PenumbraAutoRedraw : IDisposable, IRequiredService
_frame = currentFrame; _frame = currentFrame;
_framework.RunOnFrameworkThread(() => _framework.RunOnFrameworkThread(() =>
{ {
_state.ReapplyState(_objects.Player, false, StateSource.IpcManual); _state.ReapplyState(_objects.Player, false, StateSource.IpcManual, true);
Glamourer.Log.Debug( Glamourer.Log.Debug(
$"Automatically applied mod settings of type {type} to {_objects.PlayerData.Identifier.Incognito(null)} (Local Player)."); $"Automatically applied mod settings of type {type} to {_objects.PlayerData.Identifier.Incognito(null)} (Local Player).");
}); });

View file

@ -1,6 +1,8 @@
using Dalamud.Hooking; using Dalamud.Hooking;
using Dalamud.Plugin.Services; using Dalamud.Plugin.Services;
using Dalamud.Utility.Signatures; using Dalamud.Utility.Signatures;
using FFXIVClientStructs.FFXIV.Client.Game.Character;
using FFXIVClientStructs.FFXIV.Client.Game.Network;
using Glamourer.Events; using Glamourer.Events;
using Penumbra.GameData; using Penumbra.GameData;
using Penumbra.GameData.DataContainers; using Penumbra.GameData.DataContainers;
@ -14,23 +16,29 @@ public unsafe class UpdateSlotService : IDisposable
{ {
public readonly EquipSlotUpdating EquipSlotUpdatingEvent; public readonly EquipSlotUpdating EquipSlotUpdatingEvent;
public readonly BonusSlotUpdating BonusSlotUpdatingEvent; public readonly BonusSlotUpdating BonusSlotUpdatingEvent;
private readonly DictBonusItems _bonusItems; public readonly GearsetDataLoaded GearsetDataLoadedEvent;
private readonly DictBonusItems _bonusItems;
public UpdateSlotService(EquipSlotUpdating equipSlotUpdating, BonusSlotUpdating bonusSlotUpdating, IGameInteropProvider interop, public UpdateSlotService(EquipSlotUpdating equipSlotUpdating, BonusSlotUpdating bonusSlotUpdating, GearsetDataLoaded gearsetDataLoaded,
DictBonusItems bonusItems) IGameInteropProvider interop, DictBonusItems bonusItems)
{ {
EquipSlotUpdatingEvent = equipSlotUpdating; EquipSlotUpdatingEvent = equipSlotUpdating;
BonusSlotUpdatingEvent = bonusSlotUpdating; BonusSlotUpdatingEvent = bonusSlotUpdating;
_bonusItems = bonusItems; GearsetDataLoadedEvent = gearsetDataLoaded;
_bonusItems = bonusItems;
interop.InitializeFromAttributes(this); interop.InitializeFromAttributes(this);
_loadGearsetDataHook = interop.HookFromAddress<LoadGearsetDataDelegate>((nint)DrawDataContainer.MemberFunctionPointers.LoadGearsetData, LoadGearsetDataDetour);
_flagSlotForUpdateHook.Enable(); _flagSlotForUpdateHook.Enable();
_flagBonusSlotForUpdateHook.Enable(); _flagBonusSlotForUpdateHook.Enable();
_loadGearsetDataHook.Enable();
} }
public void Dispose() public void Dispose()
{ {
_flagSlotForUpdateHook.Dispose(); _flagSlotForUpdateHook.Dispose();
_flagBonusSlotForUpdateHook.Dispose(); _flagBonusSlotForUpdateHook.Dispose();
_loadGearsetDataHook.Dispose();
} }
public void UpdateEquipSlot(Model drawObject, EquipSlot slot, CharacterArmor data) public void UpdateEquipSlot(Model drawObject, EquipSlot slot, CharacterArmor data)
@ -79,6 +87,12 @@ public unsafe class UpdateSlotService : IDisposable
[Signature(Sigs.FlagBonusSlotForUpdate, DetourName = nameof(FlagBonusSlotForUpdateDetour))] [Signature(Sigs.FlagBonusSlotForUpdate, DetourName = nameof(FlagBonusSlotForUpdateDetour))]
private readonly Hook<FlagSlotForUpdateDelegateIntern> _flagBonusSlotForUpdateHook = null!; private readonly Hook<FlagSlotForUpdateDelegateIntern> _flagBonusSlotForUpdateHook = null!;
/// <summary> Detours the func that makes all FlagSlotForUpdate calls on a gearset change or initial render of a given actor (Only Cases this is Called).
/// <para> Logic done after returning the original hook executes <b>After</b> all equipment/weapon/crest data is loaded into the Actors BaseData. </para>
/// </summary>
private delegate ulong LoadGearsetDataDelegate(DrawDataContainer* drawDataContainer, PacketPlayerGearsetData* gearsetData);
private readonly Hook<LoadGearsetDataDelegate> _loadGearsetDataHook;
private ulong FlagSlotForUpdateDetour(nint drawObject, uint slotIdx, CharacterArmor* data) private ulong FlagSlotForUpdateDetour(nint drawObject, uint slotIdx, CharacterArmor* data)
{ {
var slot = slotIdx.ToEquipSlot(); var slot = slotIdx.ToEquipSlot();
@ -98,5 +112,35 @@ public unsafe class UpdateSlotService : IDisposable
} }
private ulong FlagSlotForUpdateInterop(Model drawObject, EquipSlot slot, CharacterArmor armor) 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;
}
} }

View file

@ -329,7 +329,7 @@ public class CommandService : IDisposable, IApiService
if (_stateManager.GetOrCreate(identifier, actor, out var state)) if (_stateManager.GetOrCreate(identifier, actor, out var state))
{ {
_autoDesignApplier.ReapplyAutomation(actor, identifier, state, revert, out var forcedRedraw); _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; return true;
foreach (var actor in data.Objects) 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 (!_objects.TryGetValue(identifier, out var actors))
{ {
if (_stateManager.TryGetValue(identifier, out var state)) if (_stateManager.TryGetValue(identifier, out var state))
_stateManager.ApplyDesign(state, design, ApplySettings.ManualWithLinks); _stateManager.ApplyDesign(state, design, ApplySettings.ManualWithLinks with { IsFinal = true });
} }
else else
{ {
@ -677,7 +677,7 @@ public class CommandService : IDisposable, IApiService
if (_stateManager.GetOrCreate(actor.GetIdentifier(_actors), actor, out var state)) if (_stateManager.GetOrCreate(actor.GetIdentifier(_actors), actor, out var state))
{ {
ApplyModSettings(design, actor, applyMods); ApplyModSettings(design, actor, applyMods);
_stateManager.ApplyDesign(state, design, ApplySettings.ManualWithLinks); _stateManager.ApplyDesign(state, design, ApplySettings.ManualWithLinks with { IsFinal = true });
} }
} }
} }

View file

@ -17,6 +17,7 @@ public class StateEditor(
InternalStateEditor editor, InternalStateEditor editor,
StateApplier applier, StateApplier applier,
StateChanged stateChanged, StateChanged stateChanged,
StateFinalized stateFinalized,
JobChangeState jobChange, JobChangeState jobChange,
Configuration config, Configuration config,
ItemManager items, ItemManager items,
@ -24,11 +25,12 @@ public class StateEditor(
ModSettingApplier modApplier, ModSettingApplier modApplier,
GPoseService gPose) : IDesignEditor GPoseService gPose) : IDesignEditor
{ {
protected readonly InternalStateEditor Editor = editor; protected readonly InternalStateEditor Editor = editor;
protected readonly StateApplier Applier = applier; protected readonly StateApplier Applier = applier;
protected readonly StateChanged StateChanged = stateChanged; protected readonly StateChanged StateChanged = stateChanged;
protected readonly Configuration Config = config; protected readonly StateFinalized StateFinalized = stateFinalized;
protected readonly ItemManager Items = items; protected readonly Configuration Config = config;
protected readonly ItemManager Items = items;
/// <summary> Turn an actor to. </summary> /// <summary> Turn an actor to. </summary>
public void ChangeModelId(ActorState state, uint modelId, CustomizeArray customize, nint equipData, StateSource source, public void ChangeModelId(ActorState state, uint modelId, CustomizeArray customize, nint equipData, StateSource source,
@ -41,6 +43,7 @@ public class StateEditor(
Glamourer.Log.Verbose( Glamourer.Log.Verbose(
$"Set model id in state {state.Identifier.Incognito(null)} from {old} to {modelId}. [Affecting {actors.ToLazyString("nothing")}.]"); $"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); StateChanged.Invoke(StateChangeType.Model, source, state, actors, null);
StateFinalized.Invoke(StateFinalizationType.ModelChange, actors);
} }
/// <inheritdoc/> /// <inheritdoc/>
@ -380,7 +383,7 @@ public class StateEditor(
Editor.ChangeMetaState(state, meta, mergedDesign.Design.DesignData.GetMeta(meta), Source(meta), out _, settings.Key); 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(); state.Materials.Clear();
foreach (var (key, value) in mergedDesign.Design.Materials) foreach (var (key, value) in mergedDesign.Design.Materials)
@ -417,6 +420,8 @@ public class StateEditor(
Glamourer.Log.Verbose( Glamourer.Log.Verbose(
$"Applied design to {state.Identifier.Incognito(null)}. [Affecting {actors.ToLazyString("nothing")}.]"); $"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 StateChanged.Invoke(StateChangeType.Design, state.Sources[MetaIndex.Wetness], state, actors, null); // FIXME: maybe later
if (settings.IsFinal)
StateFinalized.Invoke(StateFinalizationType.DesignApplied, actors);
return; return;
@ -437,7 +442,8 @@ public class StateEditor(
if (!settings.MergeLinks || design is not Design d) if (!settings.MergeLinks || design is not Design d)
merged = new MergedDesign(design); merged = new MergedDesign(design);
else 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); false, Config.AlwaysApplyAssociatedMods);
ApplyDesign(data, merged, settings with ApplyDesign(data, merged, settings with
@ -455,7 +461,7 @@ public class StateEditor(
if (!Config.ChangeEntireItem || !settings.Source.IsManual()) if (!Config.ChangeEntireItem || !settings.Source.IsManual())
return; return;
var mh = newMainhand ?? state.ModelData.Item(EquipSlot.MainHand); var mh = newMainhand ?? state.ModelData.Item(EquipSlot.MainHand);
// Do not change Shields to nothing. // Do not change Shields to nothing.
if (mh.Type is FullEquipType.Sword) if (mh.Type is FullEquipType.Sword)
return; return;

View file

@ -14,6 +14,7 @@ using Glamourer.GameData;
using Penumbra.GameData.DataContainers; using Penumbra.GameData.DataContainers;
using Glamourer.Designs; using Glamourer.Designs;
using Penumbra.GameData.Interop; using Penumbra.GameData.Interop;
using Glamourer.Api.Enums;
using ObjectManager = Glamourer.Interop.ObjectManager; using ObjectManager = Glamourer.Interop.ObjectManager;
namespace Glamourer.State; namespace Glamourer.State;
@ -35,10 +36,12 @@ public class StateListener : IDisposable
private readonly PenumbraService _penumbra; private readonly PenumbraService _penumbra;
private readonly EquipSlotUpdating _equipSlotUpdating; private readonly EquipSlotUpdating _equipSlotUpdating;
private readonly BonusSlotUpdating _bonusSlotUpdating; private readonly BonusSlotUpdating _bonusSlotUpdating;
private readonly GearsetDataLoaded _gearsetDataLoaded;
private readonly WeaponLoading _weaponLoading; private readonly WeaponLoading _weaponLoading;
private readonly HeadGearVisibilityChanged _headGearVisibility; private readonly HeadGearVisibilityChanged _headGearVisibility;
private readonly VisorStateChanged _visorState; private readonly VisorStateChanged _visorState;
private readonly WeaponVisibilityChanged _weaponVisibility; private readonly WeaponVisibilityChanged _weaponVisibility;
private readonly StateFinalized _stateFinalized;
private readonly AutoDesignApplier _autoDesignApplier; private readonly AutoDesignApplier _autoDesignApplier;
private readonly FunModule _funModule; private readonly FunModule _funModule;
private readonly HumanModelList _humans; private readonly HumanModelList _humans;
@ -56,11 +59,11 @@ public class StateListener : IDisposable
private ActorState? _customizeState; private ActorState? _customizeState;
public StateListener(StateManager manager, ItemManager items, PenumbraService penumbra, ActorManager actors, Configuration config, 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, WeaponVisibilityChanged weaponVisibility, HeadGearVisibilityChanged headGearVisibility, AutoDesignApplier autoDesignApplier,
FunModule funModule, HumanModelList humans, StateApplier applier, MovedEquipment movedEquipment, ObjectManager objects, FunModule funModule, HumanModelList humans, StateApplier applier, MovedEquipment movedEquipment, ObjectManager objects,
GPoseService gPose, ChangeCustomizeService changeCustomizeService, CustomizeService customizations, ICondition condition, GPoseService gPose, ChangeCustomizeService changeCustomizeService, CustomizeService customizations, ICondition condition,
CrestService crestService, BonusSlotUpdating bonusSlotUpdating) CrestService crestService, BonusSlotUpdating bonusSlotUpdating, StateFinalized stateFinalized)
{ {
_manager = manager; _manager = manager;
_items = items; _items = items;
@ -68,6 +71,7 @@ public class StateListener : IDisposable
_actors = actors; _actors = actors;
_config = config; _config = config;
_equipSlotUpdating = equipSlotUpdating; _equipSlotUpdating = equipSlotUpdating;
_gearsetDataLoaded = gearsetDataLoaded;
_weaponLoading = weaponLoading; _weaponLoading = weaponLoading;
_visorState = visorState; _visorState = visorState;
_weaponVisibility = weaponVisibility; _weaponVisibility = weaponVisibility;
@ -84,6 +88,7 @@ public class StateListener : IDisposable
_condition = condition; _condition = condition;
_crestService = crestService; _crestService = crestService;
_bonusSlotUpdating = bonusSlotUpdating; _bonusSlotUpdating = bonusSlotUpdating;
_stateFinalized = stateFinalized;
Subscribe(); Subscribe();
} }
@ -220,7 +225,7 @@ public class StateListener : IDisposable
// then we do not want to use our restricted gear protection // then we do not want to use our restricted gear protection
// since we assume the player has that gear modded to availability. // since we assume the player has that gear modded to availability.
var locked = false; var locked = false;
if (actor.Identifier(_actors, out var identifier) if (actor.Identifier(_actors, out var identifier)
&& _manager.TryGetValue(identifier, out var state)) && _manager.TryGetValue(identifier, out var state))
{ {
HandleEquipSlot(actor, state, slot, ref armor); 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) private void OnMovedEquipment((EquipSlot, uint, StainIds)[] items)
{ {
_objects.Update(); _objects.Update();
@ -787,6 +808,7 @@ public class StateListener : IDisposable
_penumbra.CreatedCharacterBase += OnCreatedCharacterBase; _penumbra.CreatedCharacterBase += OnCreatedCharacterBase;
_equipSlotUpdating.Subscribe(OnEquipSlotUpdating, EquipSlotUpdating.Priority.StateListener); _equipSlotUpdating.Subscribe(OnEquipSlotUpdating, EquipSlotUpdating.Priority.StateListener);
_bonusSlotUpdating.Subscribe(OnBonusSlotUpdating, BonusSlotUpdating.Priority.StateListener); _bonusSlotUpdating.Subscribe(OnBonusSlotUpdating, BonusSlotUpdating.Priority.StateListener);
_gearsetDataLoaded.Subscribe(OnGearsetDataLoaded, GearsetDataLoaded.Priority.StateListener);
_movedEquipment.Subscribe(OnMovedEquipment, MovedEquipment.Priority.StateListener); _movedEquipment.Subscribe(OnMovedEquipment, MovedEquipment.Priority.StateListener);
_weaponLoading.Subscribe(OnWeaponLoading, WeaponLoading.Priority.StateListener); _weaponLoading.Subscribe(OnWeaponLoading, WeaponLoading.Priority.StateListener);
_visorState.Subscribe(OnVisorChange, VisorStateChanged.Priority.StateListener); _visorState.Subscribe(OnVisorChange, VisorStateChanged.Priority.StateListener);
@ -804,6 +826,7 @@ public class StateListener : IDisposable
_penumbra.CreatedCharacterBase -= OnCreatedCharacterBase; _penumbra.CreatedCharacterBase -= OnCreatedCharacterBase;
_equipSlotUpdating.Unsubscribe(OnEquipSlotUpdating); _equipSlotUpdating.Unsubscribe(OnEquipSlotUpdating);
_bonusSlotUpdating.Unsubscribe(OnBonusSlotUpdating); _bonusSlotUpdating.Unsubscribe(OnBonusSlotUpdating);
_gearsetDataLoaded.Unsubscribe(OnGearsetDataLoaded);
_movedEquipment.Unsubscribe(OnMovedEquipment); _movedEquipment.Unsubscribe(OnMovedEquipment);
_weaponLoading.Unsubscribe(OnWeaponLoading); _weaponLoading.Unsubscribe(OnWeaponLoading);
_visorState.Unsubscribe(OnVisorChange); _visorState.Unsubscribe(OnVisorChange);

View file

@ -18,19 +18,20 @@ using Penumbra.GameData.Interop;
namespace Glamourer.State; namespace Glamourer.State;
public sealed class StateManager( public sealed class StateManager(
ActorManager _actors, ActorManager actors,
ItemManager items, ItemManager items,
StateChanged @event, StateChanged changeEvent,
StateFinalized finalizeEvent,
StateApplier applier, StateApplier applier,
InternalStateEditor editor, InternalStateEditor editor,
HumanModelList _humans, HumanModelList humans,
IClientState _clientState, IClientState clientState,
Configuration config, Configuration config,
JobChangeState jobChange, JobChangeState jobChange,
DesignMerger merger, DesignMerger merger,
ModSettingApplier modApplier, ModSettingApplier modApplier,
GPoseService gPose) GPoseService gPose)
: StateEditor(editor, applier, @event, jobChange, config, items, merger, modApplier, gPose), : StateEditor(editor, applier, changeEvent, finalizeEvent, jobChange, config, items, merger, modApplier, gPose),
IReadOnlyDictionary<ActorIdentifier, ActorState> IReadOnlyDictionary<ActorIdentifier, ActorState>
{ {
private readonly Dictionary<ActorIdentifier, ActorState> _states = []; private readonly Dictionary<ActorIdentifier, ActorState> _states = [];
@ -61,7 +62,7 @@ public sealed class StateManager(
/// <inheritdoc cref="GetOrCreate(ActorIdentifier, Actor, out ActorState?)"/> /// <inheritdoc cref="GetOrCreate(ActorIdentifier, Actor, out ActorState?)"/>
public bool GetOrCreate(Actor actor, [NotNullWhen(true)] out ActorState? state) public bool GetOrCreate(Actor actor, [NotNullWhen(true)] out ActorState? state)
=> GetOrCreate(actor.GetIdentifier(_actors), actor, out state); => GetOrCreate(actor.GetIdentifier(actors), actor, out state);
/// <summary> Try to obtain or create a new state for an existing actor. Returns false if no state could be created. </summary> /// <summary> Try to obtain or create a new state for an existing actor. Returns false if no state could be created. </summary>
public unsafe bool GetOrCreate(ActorIdentifier identifier, Actor actor, [NotNullWhen(true)] out ActorState? state) 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), ModelData = FromActor(actor, true, false),
BaseData = FromActor(actor, false, false), BaseData = FromActor(actor, false, false),
LastJob = (byte)(actor.IsCharacter ? actor.AsCharacter->CharacterData.ClassJob : 0), LastJob = (byte)(actor.IsCharacter ? actor.AsCharacter->CharacterData.ClassJob : 0),
LastTerritory = _clientState.TerritoryType, LastTerritory = clientState.TerritoryType,
}; };
// state.Identifier is owned. // state.Identifier is owned.
_states.Add(state.Identifier, state); _states.Add(state.Identifier, state);
@ -114,7 +115,7 @@ public sealed class StateManager(
// Model ID is only unambiguously contained in the game object. // Model ID is only unambiguously contained in the game object.
// The draw object only has the object type. // The draw object only has the object type.
// TODO reverse search model data to get model id from model. // 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, ret.LoadNonHuman((uint)actor.AsCharacter->ModelContainer.ModelCharaId, *(CustomizeArray*)&actor.AsCharacter->DrawData.CustomizeData,
(nint)Unsafe.AsPointer(ref actor.AsCharacter->DrawData.EquipmentModelIds[0])); (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) public void TurnHuman(ActorState state, StateSource source, uint key = 0)
=> ChangeModelId(state, 0, CustomizeArray.Default, nint.Zero, source, key); => 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)) if (!state.Unlock(key))
return; return;
@ -276,6 +277,9 @@ public sealed class StateManager(
Glamourer.Log.Verbose( Glamourer.Log.Verbose(
$"Reset entire state of {state.Identifier.Incognito(null)} to game base. [Affecting {actors.ToLazyString("nothing")}.]"); $"Reset entire state of {state.Identifier.Incognito(null)} to game base. [Affecting {actors.ToLazyString("nothing")}.]");
StateChanged.Invoke(StateChangeType.Reset, source, state, actors, null); 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) public void ResetAdvancedState(ActorState state, StateSource source, uint key = 0)
@ -301,6 +305,8 @@ public sealed class StateManager(
Glamourer.Log.Verbose( Glamourer.Log.Verbose(
$"Reset advanced customization and dye state of {state.Identifier.Incognito(null)} to game base. [Affecting {actors.ToLazyString("nothing")}.]"); $"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); 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) public void ResetCustomize(ActorState state, StateSource source, uint key = 0)
@ -318,6 +324,8 @@ public sealed class StateManager(
actors = Applier.ChangeCustomize(state, true); actors = Applier.ChangeCustomize(state, true);
Glamourer.Log.Verbose( Glamourer.Log.Verbose(
$"Reset customization state of {state.Identifier.Incognito(null)} to game base. [Affecting {actors.ToLazyString("nothing")}.]"); $"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) public void ResetEquip(ActorState state, StateSource source, uint key = 0)
@ -367,6 +375,8 @@ public sealed class StateManager(
Glamourer.Log.Verbose( Glamourer.Log.Verbose(
$"Reset equipment state of {state.Identifier.Incognito(null)} to game base. [Affecting {actors.ToLazyString("nothing")}.]"); $"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) 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)) if (!GetOrCreate(actor, out var state))
return; 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, var data = Applier.ApplyAll(state,
forceRedraw forceRedraw
|| !actor.Model.IsHuman || !actor.Model.IsHuman
|| CustomizeArray.Compare(actor.Model.GetCustomize(), state.ModelData.Customize).RequiresRedraw(), false); || CustomizeArray.Compare(actor.Model.GetCustomize(), state.ModelData.Customize).RequiresRedraw(), false);
StateChanged.Invoke(StateChangeType.Reapply, source, state, data, null); StateChanged.Invoke(StateChangeType.Reapply, source, state, data, null);
if(isFinal)
StateFinalized.Invoke(StateFinalizationType.Reapply, data);
}
/// <summary> Automation variant for reapply, to fire the correct StateUpdateType once reapplied. </summary>
public void ReapplyAutomationState(Actor actor, bool forceRedraw, bool wasReset, StateSource source)
{
if (!GetOrCreate(actor, out var state))
return;
ReapplyAutomationState(actor, state, forceRedraw, wasReset, source);
}
/// <summary> Automation variant for reapply, to fire the correct StateUpdateType once reapplied. </summary>
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) public void DeleteState(ActorIdentifier identifier)