Updated new functionality and internal gearset to use FFXIVClientStructs member functions and implemented structs, corrected remaining spacing issues.

This commit is contained in:
Cordelia Mist 2025-01-21 10:47:12 -08:00
parent 2e11481276
commit c43ce9d978
6 changed files with 33 additions and 94 deletions

View file

@ -14,7 +14,6 @@ using StateChanged = Glamourer.Events.StateChanged;
using StateUpdated = Glamourer.Events.StateUpdated; using StateUpdated = Glamourer.Events.StateUpdated;
namespace Glamourer.Api; namespace Glamourer.Api;
public sealed class StateApi : IGlamourerApiState, IApiService, IDisposable public sealed class StateApi : IGlamourerApiState, IApiService, IDisposable
{ {
private readonly ApiHelpers _helpers; private readonly ApiHelpers _helpers;
@ -340,7 +339,7 @@ public sealed class StateApi : IGlamourerApiState, IApiService, IDisposable
private void OnStateChanged(StateChangeType type, StateSource _2, ActorState _3, ActorData actors, ITransaction? _5) private void OnStateChanged(StateChangeType type, StateSource _2, ActorState _3, ActorData actors, ITransaction? _5)
{ {
// Glamourer.Log.Verbose($"[OnStateChanged] Sending out OnStateChanged with type {type}."); Glamourer.Log.Excessive($"[OnStateChanged] State Changed with Type {type} [Affecting {actors.ToLazyString("nothing")}.]");
if (StateChanged != null) if (StateChanged != null)
foreach (var actor in actors.Objects) foreach (var actor in actors.Objects)
StateChanged.Invoke(actor.Address); StateChanged.Invoke(actor.Address);
@ -352,7 +351,7 @@ public sealed class StateApi : IGlamourerApiState, IApiService, IDisposable
private void OnStateUpdated(StateUpdateType type, ActorData actors) private void OnStateUpdated(StateUpdateType type, ActorData actors)
{ {
// Glamourer.Log.Verbose($"[OnStateUpdated] Sending out OnStateUpdated with type {type}."); Glamourer.Log.Verbose($"[OnStateUpdated] State Updated with Type {type}. [Affecting {actors.ToLazyString("nothing")}.]");
if (StateUpdated != null) if (StateUpdated != null)
foreach (var actor in actors.Objects) foreach (var actor in actors.Objects)
StateUpdated.Invoke(actor.Address, type); StateUpdated.Invoke(actor.Address, type);

View file

@ -4,10 +4,10 @@ using Penumbra.GameData.Interop;
namespace Glamourer.Events; namespace Glamourer.Events;
/// <summary> /// <summary>
/// Triggers when the equipped gearset finished running all of its LoadEquipment, LoadWeapon, and crest calls. /// Triggers when the equipped gearset finished all LoadEquipment, LoadWeapon, and LoadCrest calls. (All Non-MetaData)
/// This defines a universal endpoint of base game state application to monitor. /// This defines an endpoint for when the gameState is updated.
/// <list type="number"> /// <list type="number">
/// <item>The model drawobject associated with the finished load (Also fired by other players on render) </item> /// <item>The model draw object associated with the finished load (Also fired by other players on render) </item>
/// </list> /// </list>
/// </summary> /// </summary>
public sealed class GearsetDataLoaded() public sealed class GearsetDataLoaded()
@ -18,4 +18,4 @@ public sealed class GearsetDataLoaded()
/// <seealso cref="State.StateListener.OnGearsetDataLoaded"/> /// <seealso cref="State.StateListener.OnGearsetDataLoaded"/>
StateListener = 0, StateListener = 0,
} }
} }

View file

@ -23,34 +23,26 @@ public sealed unsafe class InventoryService : IDisposable, IRequiredService
_gearsetEvent = gearsetEvent; _gearsetEvent = gearsetEvent;
_moveItemHook = interop.HookFromAddress<MoveItemDelegate>((nint)InventoryManager.MemberFunctionPointers.MoveItemSlot, MoveItemDetour); _moveItemHook = interop.HookFromAddress<MoveItemDelegate>((nint)InventoryManager.MemberFunctionPointers.MoveItemSlot, MoveItemDetour);
// This can be uncommented after ClientStructs Updates with EquipGearsetInternal after merged PR. (See comment below) _equipGearsetHook = interop.HookFromAddress<EquipGearsetInternalDelegate>((nint)RaptureGearsetModule.MemberFunctionPointers.EquipGearsetInternal, EquipGearSetDetour);
//_equipGearsetInternalHook = interop.HookFromAddress<EquipGearsetDelegate>((nint)RaptureGearsetModule.MemberFunctionPointers.EquipGearsetInternal, EquipGearSetInternalDetour);
// Can be removed after ClientStructs Update since this is only needed for current EquipGearsetInternal [Signature]
interop.InitializeFromAttributes(this);
_moveItemHook.Enable(); _moveItemHook.Enable();
_equipGearsetInternalHook.Enable(); _equipGearsetHook.Enable();
} }
public void Dispose() public void Dispose()
{ {
_moveItemHook.Dispose(); _moveItemHook.Dispose();
_equipGearsetInternalHook.Dispose(); _equipGearsetHook.Dispose();
} }
// This is the internal function processed by all sources of Equipping a gearset, such as hotbar gearset application and command gearset application.
// Currently is pending to ClientStructs for integration. See: https://github.com/aers/FFXIVClientStructs/pull/1277
public const string EquipGearsetInternal = "40 55 53 56 57 41 57 48 8D AC 24 ?? ?? ?? ?? 48 81 EC ?? ?? ?? ?? 48 8B 05 ?? ?? ?? ?? 48 33 C4 48 89 85 ?? ?? ?? ?? 4C 63 FA";
private delegate nint EquipGearsetInternalDelegate(RaptureGearsetModule* module, uint gearsetId, byte glamourPlateId); private delegate nint EquipGearsetInternalDelegate(RaptureGearsetModule* module, uint gearsetId, byte glamourPlateId);
[Signature(EquipGearsetInternal, DetourName = nameof(EquipGearSetInternalDetour))] private readonly Hook<EquipGearsetInternalDelegate> _equipGearsetHook = null!;
private readonly Hook<EquipGearsetInternalDelegate> _equipGearsetInternalHook = null!;
private nint EquipGearSetInternalDetour(RaptureGearsetModule* module, uint gearsetId, byte glamourPlateId) private nint EquipGearSetDetour(RaptureGearsetModule* module, uint gearsetId, byte glamourPlateId)
{ {
var prior = module->CurrentGearsetIndex; var prior = module->CurrentGearsetIndex;
var ret = _equipGearsetInternalHook.Original(module, gearsetId, glamourPlateId); var ret = _equipGearsetHook.Original(module, gearsetId, glamourPlateId);
var set = module->GetGearset((int)gearsetId); var set = module->GetGearset((int)gearsetId);
_gearsetEvent.Invoke(new ByteString(set->Name).ToString(), (int)gearsetId, prior, glamourPlateId, set->ClassJob); _gearsetEvent.Invoke(new ByteString(set->Name).ToString(), (int)gearsetId, prior, glamourPlateId, set->ClassJob);
Glamourer.Log.Verbose($"[InventoryService] Applied gear set {gearsetId} with glamour plate {glamourPlateId} (Returned {ret})"); Glamourer.Log.Verbose($"[InventoryService] Applied gear set {gearsetId} with glamour plate {glamourPlateId} (Returned {ret})");

View file

@ -2,6 +2,7 @@
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.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;
@ -13,22 +14,20 @@ namespace Glamourer.Interop;
public unsafe class UpdateSlotService : IDisposable public unsafe class UpdateSlotService : IDisposable
{ {
public readonly EquipSlotUpdating EquipSlotUpdatingEvent; public readonly EquipSlotUpdating EquipSlotUpdatingEvent;
public readonly BonusSlotUpdating BonusSlotUpdatingEvent; public readonly BonusSlotUpdating BonusSlotUpdatingEvent;
public readonly GearsetDataLoaded GearsetDataLoadedEvent; public readonly GearsetDataLoaded GearsetDataLoadedEvent;
private readonly DictBonusItems _bonusItems; private readonly DictBonusItems _bonusItems;
public UpdateSlotService(EquipSlotUpdating equipSlotUpdating, BonusSlotUpdating bonusSlotUpdating, GearsetDataLoaded gearsetDataLoaded, public UpdateSlotService(EquipSlotUpdating equipSlotUpdating, BonusSlotUpdating bonusSlotUpdating, GearsetDataLoaded gearsetDataLoaded,
IGameInteropProvider interop, DictBonusItems bonusItems) IGameInteropProvider interop, DictBonusItems bonusItems)
{ {
EquipSlotUpdatingEvent = equipSlotUpdating; EquipSlotUpdatingEvent = equipSlotUpdating;
BonusSlotUpdatingEvent = bonusSlotUpdating; BonusSlotUpdatingEvent = bonusSlotUpdating;
GearsetDataLoadedEvent = gearsetDataLoaded; GearsetDataLoadedEvent = gearsetDataLoaded;
_bonusItems = bonusItems; _bonusItems = bonusItems;
// Usable after the merge with client structs. _loadGearsetDataHook = interop.HookFromAddress<LoadGearsetDataDelegate>((nint)DrawDataContainer.MemberFunctionPointers.LoadGearsetData, LoadGearsetDataDetour);
//_loadGearsetDataHook = interop.HookFromAddress<LoadGearsetDataDelegate>((nint)DrawDataContainer.MemberFunctionPointers.LoadGearsetData, LoadGearsetDataDetour);
interop.InitializeFromAttributes(this); interop.InitializeFromAttributes(this);
_flagSlotForUpdateHook.Enable(); _flagSlotForUpdateHook.Enable();
_flagBonusSlotForUpdateHook.Enable(); _flagBonusSlotForUpdateHook.Enable();
_loadGearsetDataHook.Enable(); _loadGearsetDataHook.Enable();
@ -87,25 +86,15 @@ 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!;
// This signature is what calls the weapon/equipment/crest load functions in the drawData container inherited from a human/characterBase. /// <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>
// Contrary to assumption, this is not frequently fired when any slot changes, and is instead only called when another player /// </summary>
// initially loads, or when the client player changes gearsets. (Does not fire when another player or self is redrawn) private delegate Int64 LoadGearsetDataDelegate(DrawDataContainer* drawDataContainer, PacketPlayerGearsetData* gearsetData);
//
// This functions purpose is to iterate all Equipment/Weapon/Crest data on gearset change / initial player load, and determine which slots need to fire FlagSlotForUpdate.
//
// Because Glamourer processes GameState changes by detouring this method, this means by returning original after detour, any logic performed after will occur
// AFTER Glamourer finishes applying all changes to the game State, providing a gearset endpoint. (MetaData not included)
// Currently pending a merge to clientStructs, after which it can be removed, along with the explicit struct. See: https://github.com/aers/FFXIVClientStructs/pull/1277/files
public const string LoadGearsetDataSig = "48 89 5C 24 ?? 55 56 57 41 54 41 55 41 56 41 57 48 81 EC ?? ?? ?? ?? 48 8B 05 ?? ?? ?? ?? 48 33 C4 48 89 84 24 ?? ?? ?? ?? 44 0F B6 B9";
private delegate Int64 LoadGearsetDataDelegate(DrawDataContainer* drawDataContainer, GearsetDataStruct* gearsetData);
[Signature(LoadGearsetDataSig, DetourName = nameof(LoadGearsetDataDetour))]
private readonly Hook<LoadGearsetDataDelegate> _loadGearsetDataHook = null!; private readonly Hook<LoadGearsetDataDelegate> _loadGearsetDataHook = null!;
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();
var returnValue = ulong.MaxValue; var returnValue = ulong.MaxValue;
EquipSlotUpdatingEvent.Invoke(drawObject, slot, ref *data, ref returnValue); EquipSlotUpdatingEvent.Invoke(drawObject, slot, ref *data, ref returnValue);
Glamourer.Log.Excessive($"[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}).");
@ -114,7 +103,7 @@ public unsafe class UpdateSlotService : IDisposable
private ulong FlagBonusSlotForUpdateDetour(nint drawObject, uint slotIdx, CharacterArmor* data) private ulong FlagBonusSlotForUpdateDetour(nint drawObject, uint slotIdx, CharacterArmor* data)
{ {
var slot = slotIdx.ToBonusSlot(); var slot = slotIdx.ToBonusSlot();
var returnValue = ulong.MaxValue; var returnValue = ulong.MaxValue;
BonusSlotUpdatingEvent.Invoke(drawObject, slot, ref *data, ref returnValue); BonusSlotUpdatingEvent.Invoke(drawObject, slot, ref *data, ref returnValue);
Glamourer.Log.Excessive($"[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}).");
@ -123,29 +112,27 @@ public unsafe class UpdateSlotService : IDisposable
private ulong FlagSlotForUpdateInterop(Model drawObject, EquipSlot slot, CharacterArmor armor) private ulong FlagSlotForUpdateInterop(Model drawObject, EquipSlot slot, CharacterArmor armor)
{ {
Glamourer.Log.Excessive($"[FlagBonusSlotForUpdate] Invoked by Glamourer on 0x{drawObject.Address:X} on {slot} with itemdata {armor}."); Glamourer.Log.Excessive($"[FlagBonusSlotForUpdate] Glamourer-Invoked on 0x{drawObject.Address:X} on {slot} with item data {armor}.");
return _flagSlotForUpdateHook.Original(drawObject.Address, slot.ToIndex(), &armor); return _flagSlotForUpdateHook.Original(drawObject.Address, slot.ToIndex(), &armor);
} }
private Int64 LoadGearsetDataDetour(DrawDataContainer* drawDataContainer, GearsetDataStruct* gearsetData) private Int64 LoadGearsetDataDetour(DrawDataContainer* drawDataContainer, PacketPlayerGearsetData* gearsetData)
{ {
// Let the gearset data process all of its loads and slot flag update calls first.
var ret = _loadGearsetDataHook.Original(drawDataContainer, gearsetData); var ret = _loadGearsetDataHook.Original(drawDataContainer, gearsetData);
Model drawObject = drawDataContainer->OwnerObject->DrawObject; Model drawObject = drawDataContainer->OwnerObject->DrawObject;
Glamourer.Log.Verbose($"[LoadAllEquipmentDetour] Owner: 0x{drawObject.Address:X} Finished Applying its GameState!");
GearsetDataLoadedEvent.Invoke(drawObject); GearsetDataLoadedEvent.Invoke(drawObject);
// Glamourer.Log.Verbose($"[LoadAllEquipmentDetour] GearsetItemData: {FormatGearsetItemDataStruct(*gearsetData)}"); // Glamourer.Log.Excessive($"[LoadAllEquipmentDetour] GearsetItemData: {FormatGearsetItemDataStruct(*gearsetData)}");
return ret; return ret;
} }
// If you ever care to debug this, here is a formatted string output of this new gearsetData struct. // If you ever care to debug this, here is a formatted string output of this new gearsetData struct.
private string FormatGearsetItemDataStruct(GearsetDataStruct gearsetData) private string FormatGearsetItemDataStruct(PacketPlayerGearsetData gearsetData)
{ {
string ret = string ret =
$"\nMainhandWeaponData: Id: {gearsetData.MainhandWeaponData.Id}, Type: {gearsetData.MainhandWeaponData.Type}, " + $"\nMainhandWeaponData: Id: {gearsetData.MainhandWeaponData.Id}, Type: {gearsetData.MainhandWeaponData.Type}, " +
$"Variant: {gearsetData.MainhandWeaponData.Variant}, Stain0: {gearsetData.MainhandWeaponData.Stain0}, Stain1: {gearsetData.MainhandWeaponData.Stain1}" + $"Variant: {gearsetData.MainhandWeaponData.Variant}, Stain0: {gearsetData.MainhandWeaponData.Stain0}, Stain1: {gearsetData.MainhandWeaponData.Stain1}" +
$"\nOffhandWeaponData: Id: {gearsetData.OffhandWeaponData.Id}, Type: {gearsetData.OffhandWeaponData.Type}, " + $"\nOffhandWeaponData: Id: {gearsetData.OffhandWeaponData.Id}, Type: {gearsetData.OffhandWeaponData.Type}, " +
$"Variant: {gearsetData.OffhandWeaponData.Variant}, Stain0: {gearsetData.OffhandWeaponData.Stain0}, Stain1: {gearsetData.OffhandWeaponData.Stain1}" + $"Variant: {gearsetData.OffhandWeaponData.Variant}, Stain0: {gearsetData.OffhandWeaponData.Stain0}, Stain1: {gearsetData.OffhandWeaponData.Stain1}" +
$"\nCrestBitField: {gearsetData.CrestBitField} | JobId: {gearsetData.JobId} | UNK_18: {gearsetData.UNK_18} | UNK_19: {gearsetData.UNK_19}"; $"\nCrestBitField: {gearsetData.CrestBitField} | JobId: {gearsetData.JobId}";
for (int offset = 20; offset <= 56; offset += sizeof(LegacyCharacterArmor)) for (int offset = 20; offset <= 56; offset += sizeof(LegacyCharacterArmor))
{ {
LegacyCharacterArmor* equipSlotPtr = (LegacyCharacterArmor*)((byte*)&gearsetData + offset); LegacyCharacterArmor* equipSlotPtr = (LegacyCharacterArmor*)((byte*)&gearsetData + offset);
@ -156,43 +143,3 @@ public unsafe class UpdateSlotService : IDisposable
return ret; return ret;
} }
} }
// Can be removed once merged with client structs and referenced directly. See: https://github.com/aers/FFXIVClientStructs/pull/1277/files
[StructLayout(LayoutKind.Explicit)]
public readonly struct GearsetDataStruct
{
// Stores the weapon data. Includes both dyes in the data. </summary>
[FieldOffset(0)] public readonly WeaponModelId MainhandWeaponData;
[FieldOffset(8)] public readonly WeaponModelId OffhandWeaponData;
[FieldOffset(16)] public readonly byte CrestBitField; // A Bitfield:: ShieldCrest == 1, HeadCrest == 2, Chest Crest == 4
[FieldOffset(17)] public readonly byte JobId; // Job ID associated with the gearset change.
// Flicks from 0 to 127 (anywhere inbetween), have yet to associate what it is linked to. Remains the same when flicking between gearsets of the same job.
[FieldOffset(18)] public readonly byte UNK_18;
[FieldOffset(19)] public readonly byte UNK_19; // I have never seen this be anything other than 0.
// Legacy helmet equip slot armor for a character.
[FieldOffset(20)] public readonly LegacyCharacterArmor HeadSlotArmor;
[FieldOffset(24)] public readonly LegacyCharacterArmor TopSlotArmor;
[FieldOffset(28)] public readonly LegacyCharacterArmor ArmsSlotArmor;
[FieldOffset(32)] public readonly LegacyCharacterArmor LegsSlotArmor;
[FieldOffset(26)] public readonly LegacyCharacterArmor FeetSlotArmor;
[FieldOffset(40)] public readonly LegacyCharacterArmor EarSlotArmor;
[FieldOffset(44)] public readonly LegacyCharacterArmor NeckSlotArmor;
[FieldOffset(48)] public readonly LegacyCharacterArmor WristSlotArmor;
[FieldOffset(52)] public readonly LegacyCharacterArmor RFingerSlotArmor;
[FieldOffset(56)] public readonly LegacyCharacterArmor LFingerSlotArmor;
// Byte array of all slot's secondary dyes.
[FieldOffset(60)] public readonly byte HeadSlotSecondaryDye;
[FieldOffset(61)] public readonly byte TopSlotSecondaryDye;
[FieldOffset(62)] public readonly byte ArmsSlotSecondaryDye;
[FieldOffset(63)] public readonly byte LegsSlotSecondaryDye;
[FieldOffset(64)] public readonly byte FeetSlotSecondaryDye;
[FieldOffset(65)] public readonly byte EarSlotSecondaryDye;
[FieldOffset(66)] public readonly byte NeckSlotSecondaryDye;
[FieldOffset(67)] public readonly byte WristSlotSecondaryDye;
[FieldOffset(68)] public readonly byte RFingerSlotSecondaryDye;
[FieldOffset(69)] public readonly byte LFingerSlotSecondaryDye;
}

View file

@ -416,7 +416,7 @@ public class StateEditor(
var actors = settings.Source.RequiresChange() var actors = settings.Source.RequiresChange()
? Applier.ApplyAll(state, requiresRedraw, false) ? Applier.ApplyAll(state, requiresRedraw, false)
: ActorData.Invalid; : ActorData.Invalid;
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

View file

@ -225,7 +225,8 @@ 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) && _manager.TryGetValue(identifier, out var state)) if (actor.Identifier(_actors, out var identifier)
&& _manager.TryGetValue(identifier, out var state))
{ {
HandleEquipSlot(actor, state, slot, ref armor); HandleEquipSlot(actor, state, slot, ref armor);
locked = state.Sources[slot, false] is StateSource.IpcFixed; locked = state.Sources[slot, false] is StateSource.IpcFixed;