Implements true endpoints for all glamourer operations, also correctly marks reverts and gearsets. Replaced back excessive logging to maintain with logging formats expected by glamourer.

This commit is contained in:
Cordelia Mist 2025-01-17 17:39:26 -08:00
parent c605d19510
commit e1a41b5f3c
21 changed files with 225 additions and 99 deletions

View file

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

View file

@ -52,6 +52,7 @@ public sealed class IpcProviders : IDisposable, IApiService
IpcSubscribers.RevertToAutomationName.Provider(pi, api.State),
IpcSubscribers.StateChanged.Provider(pi, api.State),
IpcSubscribers.StateChangedWithType.Provider(pi, api.State),
IpcSubscribers.StateUpdated.Provider(pi, api.State),
IpcSubscribers.GPoseChanged.Provider(pi, api.State),
];
_initializedProvider.Invoke();

View file

@ -11,6 +11,7 @@ using OtterGui.Services;
using Penumbra.GameData.Interop;
using ObjectManager = Glamourer.Interop.ObjectManager;
using StateChanged = Glamourer.Events.StateChanged;
using StateUpdated = Glamourer.Events.StateUpdated;
namespace Glamourer.Api;
@ -23,6 +24,7 @@ public sealed class StateApi : IGlamourerApiState, IApiService, IDisposable
private readonly AutoDesignApplier _autoDesigns;
private readonly ObjectManager _objects;
private readonly StateChanged _stateChanged;
private readonly StateUpdated _stateUpdated;
private readonly GPoseService _gPose;
public StateApi(ApiHelpers helpers,
@ -32,6 +34,7 @@ public sealed class StateApi : IGlamourerApiState, IApiService, IDisposable
AutoDesignApplier autoDesigns,
ObjectManager objects,
StateChanged stateChanged,
StateUpdated stateUpdated,
GPoseService gPose)
{
_helpers = helpers;
@ -41,8 +44,10 @@ public sealed class StateApi : IGlamourerApiState, IApiService, IDisposable
_autoDesigns = autoDesigns;
_objects = objects;
_stateChanged = stateChanged;
_stateUpdated = stateUpdated;
_gPose = gPose;
_stateChanged.Subscribe(OnStateChanged, Events.StateChanged.Priority.GlamourerIpc);
_stateUpdated.Subscribe(OnStateUpdated, Events.StateUpdated.Priority.GlamourerIpc);
_gPose.Subscribe(OnGPoseChange, GPoseService.Priority.GlamourerIpc);
}
@ -250,13 +255,14 @@ public sealed class StateApi : IGlamourerApiState, IApiService, IDisposable
public event Action<nint>? StateChanged;
public event Action<IntPtr, StateChangeType>? StateChangedWithType;
public event Action<IntPtr, StateUpdateType>? StateUpdated;
public event Action<bool>? GPoseChanged;
private void ApplyDesign(ActorState state, DesignBase design, uint key, ApplyFlag flags)
{
var once = (flags & ApplyFlag.Once) != 0;
var settings = new ApplySettings(Source: once ? StateSource.IpcManual : StateSource.IpcFixed, Key: key, MergeLinks: true,
ResetMaterials: !once && key != 0);
ResetMaterials: !once && key != 0, SendStateUpdate: true);
_stateManager.ApplyDesign(state, design, settings);
ApiHelpers.Lock(state, key, flags);
}
@ -296,7 +302,7 @@ public sealed class StateApi : IGlamourerApiState, IApiService, IDisposable
{
var source = (flags & ApplyFlag.Once) != 0 ? StateSource.IpcManual : StateSource.IpcFixed;
_autoDesigns.ReapplyAutomation(actor, state.Identifier, state, true, out var forcedRedraw);
_stateManager.ReapplyState(actor, state, forcedRedraw, source);
_stateManager.ReapplyAutomationState(actor, state, forcedRedraw, true, source);
ApiHelpers.Lock(state, key, flags);
}
@ -333,7 +339,8 @@ public sealed class StateApi : IGlamourerApiState, IApiService, IDisposable
private void OnStateChanged(StateChangeType type, StateSource _2, ActorState _3, ActorData actors, ITransaction? _5)
{
Glamourer.Log.Error($"[OnStateChanged API CALL] Sending out OnStateChanged with type {type}.");
// Remove this comment before creating PR.
Glamourer.Log.Verbose($"[OnStateChanged] Sending out OnStateChanged with type {type}.");
if (StateChanged != null)
foreach (var actor in actors.Objects)
@ -343,4 +350,16 @@ public sealed class StateApi : IGlamourerApiState, IApiService, IDisposable
foreach (var actor in actors.Objects)
StateChangedWithType.Invoke(actor.Address, type);
}
private void OnStateUpdated(StateUpdateType type, ActorData actors)
{
if (StateUpdated != null)
foreach (var actor in actors.Objects)
{
// Remove these before creating PR
Glamourer.Log.Information($"[ENDPOINT DEBUGGING] 0x{actor.Address:X} had update of type {type}.");
Glamourer.Log.Information("--------------------------------------------------------------");
StateUpdated.Invoke(actor.Address, type);
}
}
}

View file

@ -216,8 +216,6 @@ public sealed class AutoDesignApplier : IDisposable
if (!_config.EnableAutoDesigns || !actor.Identifier(_actors, out var id))
return;
Glamourer.Log.Information($"[AutoDesignApplier][OnJobChange] We had EnableAutoDesigns active, and are a valid actor!");
if (!GetPlayerSet(id, out var set))
{
if (_state.TryGetValue(id, out var s))

View file

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

View file

@ -0,0 +1,23 @@
using OtterGui.Classes;
using Penumbra.GameData.Enums;
using Penumbra.GameData.Interop;
using Penumbra.GameData.Structs;
namespace Glamourer.Events;
/// <summary>
/// Triggers when the equipped gearset finished running all of its LoadEquipment, LoadWeapon, and crest calls.
/// This defines a universal endpoint of base game state application to monitor.
/// <list type="number">
/// <item>The model drawobject associated with the finished load (should always be ClientPlayer) </item>
/// </list>
/// </summary>
public sealed class GearsetDataLoaded()
: EventWrapper<Model, GearsetDataLoaded.Priority>(nameof(GearsetDataLoaded))
{
public enum Priority
{
/// <seealso cref="State.StateListener.OnEquippedGearsetLoaded"/>
StateListener = 0,
}
}

View file

@ -0,0 +1,26 @@
using Glamourer.Api.Enums;
using Glamourer.Designs.History;
using Glamourer.Interop.Structs;
using Glamourer.State;
using OtterGui.Classes;
namespace Glamourer.Events;
/// <summary>
/// Triggered when a Design is edited in any way.
/// <list type="number">
/// <item>Parameter is the type of the change </item>
/// <item>Parameter is the changed saved state. </item>
/// <item>Parameter is the existing actors using this saved state. </item>
/// <item>Parameter is any additional data depending on the type of change. </item>
/// </list>
/// </summary>
public sealed class StateUpdated()
: EventWrapper<StateUpdateType, ActorData, StateUpdated.Priority>(nameof(StateUpdated))
{
public enum Priority
{
/// <seealso cref="Api.GlamourerIpc.OnStateUpdated"/>
GlamourerIpc = int.MinValue,
}
}

View file

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

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

View file

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

View file

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

View file

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

View file

@ -70,7 +70,7 @@ public unsafe class ChangeCustomizeService : EventWrapperRef2<Model, CustomizeAr
if (!model.IsHuman)
return false;
Glamourer.Log.Information($"[ChangeCustomize] Glamour-Invoked on 0x{model.Address:X} with {customize}.");
Glamourer.Log.Verbose($"[ChangeCustomize] Invoked on 0x{model.Address:X} with {customize}.");
using var _ = InUpdate.EnterMethod();
var ret = _original(model.AsHuman, customize.Data, true);
return ret;
@ -86,8 +86,6 @@ public unsafe class ChangeCustomizeService : EventWrapperRef2<Model, CustomizeAr
Invoke(human, ref *(CustomizeArray*)data);
var ret = _changeCustomizeHook.Original(human, data, skipEquipment);
Glamourer.Log.Information($"[ChangeCustomize] Called on with {*(CustomizeArray*)data} ({ret}).");
_postEvent.Invoke(human);
return ret;
}

View file

@ -18,7 +18,9 @@ public sealed unsafe class InventoryService : IDisposable, IRequiredService
private readonly EquippedGearset _gearsetEvent;
private readonly List<(EquipSlot, uint, StainIds)> _itemList = new(12);
// This can be moved into client structs or penumbra.gamedata when needed.
// Called by EquipGearset, but returns a pointer instead of an int.
// This is the internal function processed by all sources of Equipping a gearset,
// such as hotbar gearset application and command gearset application
public const string EquipGearsetInternal = "40 55 53 56 57 41 57 48 8D AC 24 ?? ?? ?? ?? 48 81 EC ?? ?? ?? ?? 48 8B 05 ?? ?? ?? ?? 48 33 C4 48 89 85 ?? ?? ?? ?? 4C 63 FA";
private delegate nint ChangeGearsetInternalDelegate(RaptureGearsetModule* module, uint gearsetId, byte glamourPlateId);
@ -56,7 +58,7 @@ public sealed unsafe class InventoryService : IDisposable, IRequiredService
var ret = _equipGearsetInternalHook.Original(module, gearsetId, glamourPlateId);
var set = module->GetGearset((int)gearsetId);
_gearsetEvent.Invoke(new ByteString(set->Name).ToString(), (int)gearsetId, prior, glamourPlateId, set->ClassJob);
Glamourer.Log.Warning($"[InventoryService] [EquipInternal] Applied gear set {gearsetId} with glamour plate {glamourPlateId} (Returned {ret})");
Glamourer.Log.Verbose($"[InventoryService] [EquipInternal] Applied gear set {gearsetId} with glamour plate {glamourPlateId} (Returned {ret})");
if (ret == 0)
{
var entry = module->GetGearset((int)gearsetId);
@ -132,7 +134,7 @@ public sealed unsafe class InventoryService : IDisposable, IRequiredService
private int EquipGearSetDetour(RaptureGearsetModule* module, int gearsetId, byte glamourPlateId)
{
var ret = _equipGearsetHook.Original(module, gearsetId, glamourPlateId);
Glamourer.Log.Verbose($"[InventoryService] Applied gear set {gearsetId} with glamour plate {glamourPlateId} (Returned {ret})");
Glamourer.Log.Excessive($"[InventoryService] (old) Applied gear set {gearsetId} with glamour plate {glamourPlateId} (Returned {ret})");
return ret;
}
@ -148,7 +150,7 @@ public sealed unsafe class InventoryService : IDisposable, IRequiredService
InventoryType targetContainer, ushort targetSlot, byte unk)
{
var ret = _moveItemHook.Original(manager, sourceContainer, sourceSlot, targetContainer, targetSlot, unk);
Glamourer.Log.Verbose($"[InventoryService] Moved {sourceContainer} {sourceSlot} {targetContainer} {targetSlot} (Returned {ret})");
Glamourer.Log.Excessive($"[InventoryService] Moved {sourceContainer} {sourceSlot} {targetContainer} {targetSlot} (Returned {ret})");
if (ret == 0)
{
if (InvokeSource(sourceContainer, sourceSlot, out var source))

View file

@ -50,7 +50,7 @@ public class JobService : IDisposable
var newJob = Jobs.TryGetValue(newJobIndex, out var j) ? j : Jobs[0];
var oldJob = Jobs.TryGetValue(oldJobIndex, out var o) ? o : Jobs[0];
Glamourer.Log.Error($"{actor} changed job from {oldJob} to {newJob}.");
Glamourer.Log.Excessive($"{actor} changed job from {oldJob} to {newJob}.");
JobChanged?.Invoke(actor, oldJob, newJob);
}
}

View file

@ -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).");
});

View file

@ -23,7 +23,7 @@ public readonly struct GearsetItemDataStruct
[FieldOffset(16)] public readonly byte CrestBitField; // A Bitfield:: ShieldCrest == 1, HeadCrest == 2, Chest Crest == 4
[FieldOffset(17)] public readonly byte JobId; // Job ID associated with the gearset change.
// Flicks from 0 to 128 (anywhere inbetween), have yet to associate what it is linked to. Remains the same when flicking between gearsets of the same job.
// Flicks from 0 to 127 (anywhere inbetween), have yet to associate what it is linked to. Remains the same when flicking between gearsets of the same job.
[FieldOffset(18)] public readonly byte UNK_18;
[FieldOffset(19)] public readonly byte UNK_19; // I have never seen this be anything other than 0.
@ -56,69 +56,47 @@ public unsafe class UpdateSlotService : IDisposable
{
public readonly EquipSlotUpdating EquipSlotUpdatingEvent;
public readonly BonusSlotUpdating BonusSlotUpdatingEvent;
public readonly GearsetDataLoaded GearsetDataLoadedEvent;
private readonly DictBonusItems _bonusItems;
#region LoadAllEquipData
///////////////////////////////////////////////////
// This is a currently undocumented signature that loads all equipment after changing a gearset.
// :: Signature Maintainers Note:
// To obtain this signature, get the stacktrace from FlagSlotForUpdate for human, and find func `sub_140842F50`.
// This function is what calls the weapon/equipment/crest loads, which call FlagSlotForUpdate if different.
//
// By detouring this function, and executing the original, then logic after, we have a consistant point in time where we know all
// slots have been flagged, meaning a consistant point in time that glamourer has processed all of its updates.
public const string LoadAllEquipmentSig = "48 89 5C 24 ?? 55 56 57 41 54 41 55 41 56 41 57 48 81 EC ?? ?? ?? ?? 48 8B 05 ?? ?? ?? ?? 48 33 C4 48 89 84 24 ?? ?? ?? ?? 44 0F B6 B9";
private delegate Int64 LoadAllEquipmentDelegate(DrawDataContainer* drawDataContainer, GearsetItemDataStruct* gearsetData);
private Int64 LoadAllEquipmentDetour(DrawDataContainer* drawDataContainer, GearsetItemDataStruct* gearsetData)
// This function is what calls the weapon/equipment/crest loads, which call FlagSlotForUpdate if different. (MetaData not included)
public const string LoadGearsetDataSig = "48 89 5C 24 ?? 55 56 57 41 54 41 55 41 56 41 57 48 81 EC ?? ?? ?? ?? 48 8B 05 ?? ?? ?? ?? 48 33 C4 48 89 84 24 ?? ?? ?? ?? 44 0F B6 B9";
private delegate Int64 LoadGearsetDataDelegate(DrawDataContainer* drawDataContainer, GearsetItemDataStruct* gearsetData);
private Int64 LoadGearsetDataDetour(DrawDataContainer* drawDataContainer, GearsetItemDataStruct* gearsetData)
{
// return original first so we can log the changes after
var ret = _loadAllEquipmentHook.Original(drawDataContainer, gearsetData);
// Let the gearset data process all of its loads and slot flag update calls first.
var ret = _loadGearsetDataHook.Original(drawDataContainer, gearsetData);
// Ensure that the owner of the drawdata container is a character base.
Model ownerDrawObject = drawDataContainer->OwnerObject->DrawObject;
if (!ownerDrawObject.IsCharacterBase)
return ret;
// perform logic stuff.
var owner = drawDataContainer->OwnerObject;
Glamourer.Log.Warning($"[LoadAllEquipmentDetour] Owner: 0x{(nint)owner->DrawObject:X} Finished Applying its GameState!");
Glamourer.Log.Warning($"[LoadAllEquipmentDetour] GearsetItemData: {FormatGearsetItemDataStruct(*gearsetData)}");
// return original.
// invoke the changed event for the state listener and return.
Glamourer.Log.Verbose($"[LoadAllEquipmentDetour] Owner: 0x{ownerDrawObject.Address:X} Finished Applying its GameState!");
// Glamourer.Log.Verbose($"[LoadAllEquipmentDetour] GearsetItemData: {FormatGearsetItemDataStruct(*gearsetData)}");
GearsetDataLoadedEvent.Invoke(drawDataContainer->OwnerObject->DrawObject);
return ret;
}
private string FormatWeaponModelId(WeaponModelId weaponModelId) => $"Id: {weaponModelId.Id}, Type: {weaponModelId.Type}, Variant: {weaponModelId.Variant}, Stain0: {weaponModelId.Stain0}, Stain1: {weaponModelId.Stain1}";
private string FormatGearsetItemDataStruct(GearsetItemDataStruct gearsetItemData)
{
string ret = $"\nMainhandWeaponData: {FormatWeaponModelId(gearsetItemData.MainhandWeaponData)}," +
$"\nOffhandWeaponData: {FormatWeaponModelId(gearsetItemData.OffhandWeaponData)}," +
$"\nCrestBitField: {gearsetItemData.CrestBitField} | JobId: {gearsetItemData.JobId} | UNK_18: {gearsetItemData.UNK_18} | UNK_19: {gearsetItemData.UNK_19}";
// Iterate through offsets from 20 to 60 and format the CharacterArmor data
for (int offset = 20; offset <= 56; offset += sizeof(LegacyCharacterArmor))
{
LegacyCharacterArmor* equipSlotPtr = (LegacyCharacterArmor*)((byte*)&gearsetItemData + offset);
int dyeOffset = (offset - 20) / sizeof(LegacyCharacterArmor) + 60; // Calculate the corresponding dye offset
byte* dyePtr = (byte*)&gearsetItemData + dyeOffset;
ret += $"\nEquipSlot {((EquipSlot)(dyeOffset-60)).ToString()}:: Id: {(*equipSlotPtr).Set}, Variant: {(*equipSlotPtr).Variant}, Stain0: {(*equipSlotPtr).Stain.Id}, Stain1: {*dyePtr}";
}
return ret;
}
#endregion LoadAllEquipData
public UpdateSlotService(EquipSlotUpdating equipSlotUpdating, BonusSlotUpdating bonusSlotUpdating, IGameInteropProvider interop,
DictBonusItems bonusItems)
public UpdateSlotService(EquipSlotUpdating equipSlotUpdating, BonusSlotUpdating bonusSlotUpdating, GearsetDataLoaded gearsetDataLoaded,
IGameInteropProvider interop, DictBonusItems bonusItems)
{
EquipSlotUpdatingEvent = equipSlotUpdating;
BonusSlotUpdatingEvent = bonusSlotUpdating;
GearsetDataLoadedEvent = gearsetDataLoaded;
_bonusItems = bonusItems;
interop.InitializeFromAttributes(this);
_flagSlotForUpdateHook.Enable();
_flagBonusSlotForUpdateHook.Enable();
_loadAllEquipmentHook.Enable();
_loadGearsetDataHook.Enable();
}
public void Dispose()
{
_flagSlotForUpdateHook.Dispose();
_flagBonusSlotForUpdateHook.Dispose();
_loadAllEquipmentHook.Dispose();
_loadGearsetDataHook.Dispose();
}
public void UpdateEquipSlot(Model drawObject, EquipSlot slot, CharacterArmor data)
@ -167,18 +145,16 @@ public unsafe class UpdateSlotService : IDisposable
[Signature(Sigs.FlagBonusSlotForUpdate, DetourName = nameof(FlagBonusSlotForUpdateDetour))]
private readonly Hook<FlagSlotForUpdateDelegateIntern> _flagBonusSlotForUpdateHook = null!;
[Signature(LoadAllEquipmentSig, DetourName = nameof(LoadAllEquipmentDetour))]
private readonly Hook<LoadAllEquipmentDelegate> _loadAllEquipmentHook = null!;
[Signature(LoadGearsetDataSig, DetourName = nameof(LoadGearsetDataDetour))]
private readonly Hook<LoadGearsetDataDelegate> _loadGearsetDataHook = null!;
private ulong FlagSlotForUpdateDetour(nint drawObject, uint slotIdx, CharacterArmor* data)
{
var slot = slotIdx.ToEquipSlot();
var returnValue = ulong.MaxValue;
EquipSlotUpdatingEvent.Invoke(drawObject, slot, ref *data, ref returnValue);
Glamourer.Log.Information($"[FlagSlotForUpdate] Called with 0x{drawObject:X} for slot {slot} with {*data} ({returnValue:X}).");
Glamourer.Log.Excessive($"[FlagSlotForUpdate] Called with 0x{drawObject:X} for slot {slot} with {*data} ({returnValue:X}).");
returnValue = returnValue == ulong.MaxValue ? _flagSlotForUpdateHook.Original(drawObject, slotIdx, data) : returnValue;
return returnValue;
}
@ -186,17 +162,35 @@ public unsafe class UpdateSlotService : IDisposable
{
var slot = slotIdx.ToBonusSlot();
var returnValue = ulong.MaxValue;
BonusSlotUpdatingEvent.Invoke(drawObject, slot, ref *data, ref returnValue);
Glamourer.Log.Information($"[FlagBonusSlotForUpdate] Called with 0x{drawObject:X} for slot {slot} with {*data} ({returnValue:X}).");
Glamourer.Log.Excessive($"[FlagBonusSlotForUpdate] Called with 0x{drawObject:X} for slot {slot} with {*data} ({returnValue:X}).");
returnValue = returnValue == ulong.MaxValue ? _flagBonusSlotForUpdateHook.Original(drawObject, slotIdx, data) : returnValue;
return returnValue;
}
private ulong FlagSlotForUpdateInterop(Model drawObject, EquipSlot slot, CharacterArmor armor)
{
Glamourer.Log.Warning($"Glamour-Invoked Equip Slot update for 0x{drawObject.Address:X} with {slot} and {armor}.");
Glamourer.Log.Excessive($"[FlagBonusSlotForUpdate] Invoked by Glamourer on 0x{drawObject.Address:X} on {slot} with itemdata {armor}.");
return _flagSlotForUpdateHook.Original(drawObject.Address, slot.ToIndex(), &armor);
}
// If you ever care to debug this, here is a formatted string output of this new gearsetDataPacket struct.
private string FormatGearsetItemDataStruct(GearsetItemDataStruct gearsetData)
{
string ret =
$"\nMainhandWeaponData: Id: {gearsetData.MainhandWeaponData.Id}, Type: {gearsetData.MainhandWeaponData.Type}, " +
$"Variant: {gearsetData.MainhandWeaponData.Variant}, Stain0: {gearsetData.MainhandWeaponData.Stain0}, Stain1: {gearsetData.MainhandWeaponData.Stain1}" +
$"\nOffhandWeaponData: Id: {gearsetData.OffhandWeaponData.Id}, Type: {gearsetData.OffhandWeaponData.Type}, " +
$"Variant: {gearsetData.OffhandWeaponData.Variant}, Stain0: {gearsetData.OffhandWeaponData.Stain0}, Stain1: {gearsetData.OffhandWeaponData.Stain1}" +
$"\nCrestBitField: {gearsetData.CrestBitField} | JobId: {gearsetData.JobId} | UNK_18: {gearsetData.UNK_18} | UNK_19: {gearsetData.UNK_19}";
// Iterate through offsets from 20 to 60 and format the CharacterArmor data
for (int offset = 20; offset <= 56; offset += sizeof(LegacyCharacterArmor))
{
LegacyCharacterArmor* equipSlotPtr = (LegacyCharacterArmor*)((byte*)&gearsetData + offset);
int dyeOffset = (offset - 20) / sizeof(LegacyCharacterArmor) + 60; // Calculate the corresponding dye offset
byte* dyePtr = (byte*)&gearsetData + dyeOffset;
ret += $"\nEquipSlot {((EquipSlot)(dyeOffset - 60)).ToString()}:: Id: {(*equipSlotPtr).Set}, Variant: {(*equipSlotPtr).Variant}, Stain0: {(*equipSlotPtr).Stain.Id}, Stain1: {*dyePtr}";
}
return ret;
}
}

View file

@ -329,7 +329,7 @@ public class CommandService : IDisposable, IApiService
if (_stateManager.GetOrCreate(identifier, actor, out var state))
{
_autoDesignApplier.ReapplyAutomation(actor, identifier, state, revert, out var forcedRedraw);
_stateManager.ReapplyState(actor, forcedRedraw, StateSource.Manual);
_stateManager.ReapplyAutomationState(actor, forcedRedraw, revert, StateSource.Manual);
}
}
}
@ -378,7 +378,7 @@ public class CommandService : IDisposable, IApiService
return true;
foreach (var actor in data.Objects)
_stateManager.ReapplyState(actor, false, StateSource.Manual);
_stateManager.ReapplyState(actor, false, StateSource.Manual, true);
}
@ -668,7 +668,7 @@ public class CommandService : IDisposable, IApiService
if (!_objects.TryGetValue(identifier, out var actors))
{
if (_stateManager.TryGetValue(identifier, out var state))
_stateManager.ApplyDesign(state, design, ApplySettings.ManualWithLinks);
_stateManager.ApplyDesign(state, design, ApplySettings.ManualWithLinks with { SendStateUpdate = true });
}
else
{
@ -677,7 +677,7 @@ public class CommandService : IDisposable, IApiService
if (_stateManager.GetOrCreate(actor.GetIdentifier(_actors), actor, out var state))
{
ApplyModSettings(design, actor, applyMods);
_stateManager.ApplyDesign(state, design, ApplySettings.ManualWithLinks);
_stateManager.ApplyDesign(state, design, ApplySettings.ManualWithLinks with { SendStateUpdate = true });
}
}
}

View file

@ -17,6 +17,7 @@ public class StateEditor(
InternalStateEditor editor,
StateApplier applier,
StateChanged stateChanged,
StateUpdated stateUpdated,
JobChangeState jobChange,
Configuration config,
ItemManager items,
@ -27,6 +28,7 @@ public class StateEditor(
protected readonly InternalStateEditor Editor = editor;
protected readonly StateApplier Applier = applier;
protected readonly StateChanged StateChanged = stateChanged;
protected readonly StateUpdated StateUpdated = stateUpdated;
protected readonly Configuration Config = config;
protected readonly ItemManager Items = items;
@ -41,6 +43,7 @@ public class StateEditor(
Glamourer.Log.Verbose(
$"Set model id in state {state.Identifier.Incognito(null)} from {old} to {modelId}. [Affecting {actors.ToLazyString("nothing")}.]");
StateChanged.Invoke(StateChangeType.Model, source, state, actors, null);
StateUpdated.Invoke(StateUpdateType.ModelChange, actors);
}
/// <inheritdoc/>
@ -419,6 +422,8 @@ public class StateEditor(
Glamourer.Log.Debug($"Applied design to {state.Identifier.Incognito(null)}. [Affecting {actors.ToLazyString("nothing")}.]");
StateChanged.Invoke(StateChangeType.Design, state.Sources[MetaIndex.Wetness], state, actors, null); // FIXME: maybe later
if(settings.SendStateUpdate)
StateUpdated.Invoke(StateUpdateType.DesignApplied, actors);
return;

View file

@ -14,6 +14,7 @@ using Penumbra.GameData.DataContainers;
using Glamourer.Designs;
using Penumbra.GameData.Interop;
using ObjectManager = Glamourer.Interop.ObjectManager;
using Glamourer.Api.Enums;
namespace Glamourer.State;
@ -34,10 +35,12 @@ public class StateListener : IDisposable
private readonly PenumbraService _penumbra;
private readonly EquipSlotUpdating _equipSlotUpdating;
private readonly BonusSlotUpdating _bonusSlotUpdating;
private readonly GearsetDataLoaded _gearsetDataLoaded;
private readonly WeaponLoading _weaponLoading;
private readonly HeadGearVisibilityChanged _headGearVisibility;
private readonly VisorStateChanged _visorState;
private readonly WeaponVisibilityChanged _weaponVisibility;
private readonly StateUpdated _stateUpdated;
private readonly AutoDesignApplier _autoDesignApplier;
private readonly FunModule _funModule;
private readonly HumanModelList _humans;
@ -54,11 +57,11 @@ public class StateListener : IDisposable
private ActorState? _customizeState;
public StateListener(StateManager manager, ItemManager items, PenumbraService penumbra, ActorManager actors, Configuration config,
EquipSlotUpdating equipSlotUpdating, WeaponLoading weaponLoading, VisorStateChanged visorState,
EquipSlotUpdating equipSlotUpdating, GearsetDataLoaded gearsetDataLoaded, WeaponLoading weaponLoading, VisorStateChanged visorState,
WeaponVisibilityChanged weaponVisibility, HeadGearVisibilityChanged headGearVisibility, AutoDesignApplier autoDesignApplier,
FunModule funModule, HumanModelList humans, StateApplier applier, MovedEquipment movedEquipment, ObjectManager objects,
GPoseService gPose, ChangeCustomizeService changeCustomizeService, CustomizeService customizations, ICondition condition,
CrestService crestService, BonusSlotUpdating bonusSlotUpdating)
CrestService crestService, BonusSlotUpdating bonusSlotUpdating, StateUpdated stateUpdated)
{
_manager = manager;
_items = items;
@ -66,6 +69,7 @@ public class StateListener : IDisposable
_actors = actors;
_config = config;
_equipSlotUpdating = equipSlotUpdating;
_gearsetDataLoaded = gearsetDataLoaded;
_weaponLoading = weaponLoading;
_visorState = visorState;
_weaponVisibility = weaponVisibility;
@ -82,6 +86,7 @@ public class StateListener : IDisposable
_condition = condition;
_crestService = crestService;
_bonusSlotUpdating = bonusSlotUpdating;
_stateUpdated = stateUpdated;
Subscribe();
}
@ -259,6 +264,22 @@ public class StateListener : IDisposable
}
}
private void OnGearsetDataLoaded(Model model)
{
var actor = _penumbra.GameObjectFromDrawObject(model);
if (_condition[ConditionFlag.CreatingCharacter] && actor.Index >= ObjectIndex.CutsceneStart)
return;
// ensure actor and state are valid.
if (!actor.Identifier(_actors, out var identifier))
return;
_objects.Update();
if (_objects.TryGetValue(identifier, out var actors) && actors.Valid)
_stateUpdated.Invoke(StateUpdateType.Gearset, actors);
}
private void OnMovedEquipment((EquipSlot, uint, StainIds)[] items)
{
_objects.Update();
@ -382,7 +403,7 @@ public class StateListener : IDisposable
lastFistOffhand = new CharacterWeapon((PrimaryId)(weapon.Skeleton.Id + 50), weapon.Weapon, weapon.Variant,
weapon.Stains);
_fistOffhands[actor] = lastFistOffhand;
Glamourer.Log.Verbose($"Storing fist weapon offhand {lastFistOffhand} for 0x{actor.Address:X}.");
Glamourer.Log.Excessive($"Storing fist weapon offhand {lastFistOffhand} for 0x{actor.Address:X}.");
}
_funModule.ApplyFunToWeapon(actor, ref weapon, slot);
@ -765,6 +786,7 @@ public class StateListener : IDisposable
_penumbra.CreatedCharacterBase += OnCreatedCharacterBase;
_equipSlotUpdating.Subscribe(OnEquipSlotUpdating, EquipSlotUpdating.Priority.StateListener);
_bonusSlotUpdating.Subscribe(OnBonusSlotUpdating, BonusSlotUpdating.Priority.StateListener);
_gearsetDataLoaded.Subscribe(OnGearsetDataLoaded, GearsetDataLoaded.Priority.StateListener);
_movedEquipment.Subscribe(OnMovedEquipment, MovedEquipment.Priority.StateListener);
_weaponLoading.Subscribe(OnWeaponLoading, WeaponLoading.Priority.StateListener);
_visorState.Subscribe(OnVisorChange, VisorStateChanged.Priority.StateListener);
@ -782,6 +804,7 @@ public class StateListener : IDisposable
_penumbra.CreatedCharacterBase -= OnCreatedCharacterBase;
_equipSlotUpdating.Unsubscribe(OnEquipSlotUpdating);
_bonusSlotUpdating.Unsubscribe(OnBonusSlotUpdating);
_gearsetDataLoaded.Unsubscribe(OnGearsetDataLoaded);
_movedEquipment.Unsubscribe(OnMovedEquipment);
_weaponLoading.Unsubscribe(OnWeaponLoading);
_visorState.Unsubscribe(OnVisorChange);

View file

@ -21,6 +21,7 @@ public sealed class StateManager(
ActorManager _actors,
ItemManager items,
StateChanged @event,
StateUpdated @event2,
StateApplier applier,
InternalStateEditor editor,
HumanModelList _humans,
@ -30,7 +31,7 @@ public sealed class StateManager(
DesignMerger merger,
ModSettingApplier modApplier,
GPoseService gPose)
: StateEditor(editor, applier, @event, jobChange, config, items, merger, modApplier, gPose),
: StateEditor(editor, applier, @event, @event2, jobChange, config, items, merger, modApplier, gPose),
IReadOnlyDictionary<ActorIdentifier, ActorState>
{
private readonly Dictionary<ActorIdentifier, ActorState> _states = [];
@ -235,7 +236,7 @@ public sealed class StateManager(
public void TurnHuman(ActorState state, StateSource source, uint key = 0)
=> ChangeModelId(state, 0, CustomizeArray.Default, nint.Zero, source, key);
public void ResetState(ActorState state, StateSource source, uint key = 0)
public void ResetState(ActorState state, StateSource source, uint key = 0, bool stateUpdate = false)
{
if (!state.Unlock(key))
return;
@ -276,6 +277,9 @@ public sealed class StateManager(
Glamourer.Log.Debug(
$"Reset entire state of {state.Identifier.Incognito(null)} to game base. [Affecting {actors.ToLazyString("nothing")}.]");
StateChanged.Invoke(StateChangeType.Reset, source, state, actors, null);
// only invoke if we define this reset call as the final call in our state update.
if(stateUpdate)
StateUpdated.Invoke(StateUpdateType.Revert, actors);
}
public void ResetAdvancedState(ActorState state, StateSource source, uint key = 0)
@ -301,6 +305,8 @@ public sealed class StateManager(
Glamourer.Log.Debug(
$"Reset advanced customization and dye state of {state.Identifier.Incognito(null)} to game base. [Affecting {actors.ToLazyString("nothing")}.]");
StateChanged.Invoke(StateChangeType.Reset, source, state, actors, null);
// Update that we have completed a full operation. (We can do this directly as nothing else is linked)
StateUpdated.Invoke(StateUpdateType.RevertAdvanced, actors);
}
public void ResetCustomize(ActorState state, StateSource source, uint key = 0)
@ -318,6 +324,8 @@ public sealed class StateManager(
actors = Applier.ChangeCustomize(state, true);
Glamourer.Log.Verbose(
$"Reset customization state of {state.Identifier.Incognito(null)} to game base. [Affecting {actors.ToLazyString("nothing")}.]");
// Update that we have completed a full operation. (We can do this directly as nothing else is linked)
StateUpdated.Invoke(StateUpdateType.RevertCustomize, actors);
}
public void ResetEquip(ActorState state, StateSource source, uint key = 0)
@ -367,6 +375,8 @@ public sealed class StateManager(
Glamourer.Log.Verbose(
$"Reset equipment state of {state.Identifier.Incognito(null)} to game base. [Affecting {actors.ToLazyString("nothing")}.]");
// Update that we have completed a full operation. (We can do this directly as nothing else is linked)
StateUpdated.Invoke(StateUpdateType.RevertEquipment, actors);
}
public void ResetStateFixed(ActorState state, bool respectManualPalettes, uint key = 0)
@ -443,21 +453,44 @@ public sealed class StateManager(
}
}
public void ReapplyState(Actor actor, bool forceRedraw, StateSource source)
public void ReapplyState(Actor actor, bool forceRedraw, StateSource source, bool isUpdate = false)
{
if (!GetOrCreate(actor, out var state))
return;
ReapplyState(actor, state, forceRedraw, source);
ReapplyState(actor, state, forceRedraw, source, isUpdate);
}
public void ReapplyState(Actor actor, ActorState state, bool forceRedraw, StateSource source)
public void ReapplyState(Actor actor, ActorState state, bool forceRedraw, StateSource source, bool isUpdate)
{
var data = Applier.ApplyAll(state,
forceRedraw
|| !actor.Model.IsHuman
|| CustomizeArray.Compare(actor.Model.GetCustomize(), state.ModelData.Customize).RequiresRedraw(), false);
StateChanged.Invoke(StateChangeType.Reapply, source, state, data, null);
if(isUpdate)
StateUpdated.Invoke(StateUpdateType.Reapply, data);
}
/// <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.
StateUpdated.Invoke(wasReset ? StateUpdateType.RevertAutomation : StateUpdateType.ReapplyAutomation, data);
}
public void DeleteState(ActorIdentifier identifier)