This commit is contained in:
Ottermandias 2023-06-09 17:57:40 +02:00
parent 7710cfadfa
commit 2d6fd6015d
88 changed files with 2304 additions and 383 deletions

View file

@ -1,165 +0,0 @@
using System;
using Dalamud.Game.ClientState.Objects.Enums;
using Dalamud.Game.ClientState.Objects.Types;
using Glamourer.Customization;
using Penumbra.GameData.Actors;
using Penumbra.GameData.Structs;
using Penumbra.String;
using CustomizeData = Penumbra.GameData.Structs.CustomizeData;
namespace Glamourer.Interop;
public unsafe partial struct Actor : IEquatable<Actor>, IDesignable
{
public static readonly Actor Null = new() { Pointer = null };
public FFXIVClientStructs.FFXIV.Client.Game.Character.Character* Pointer;
public IntPtr Address
=> (IntPtr)Pointer;
public static implicit operator Actor(IntPtr? pointer)
=> new() { Pointer = (FFXIVClientStructs.FFXIV.Client.Game.Character.Character*)(pointer ?? IntPtr.Zero) };
public static implicit operator IntPtr(Actor actor)
=> actor.Pointer == null ? IntPtr.Zero : (IntPtr)actor.Pointer;
public ActorIdentifier GetIdentifier(ActorManager actors)
=> actors.FromObject((FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject*)Pointer, out _, true, true, false);
public bool Identifier(ActorManager actors, out ActorIdentifier ident)
{
if (Valid)
{
ident = GetIdentifier(actors);
return true;
}
ident = ActorIdentifier.Invalid;
return false;
}
public override string ToString()
=> Pointer != null ? Utf8Name.ToString() : "Invalid";
public bool IsAvailable
=> Pointer->GameObject.GetIsTargetable();
public bool IsHuman
=> Pointer != null && Pointer->ModelCharaId == 0;
public ObjectKind ObjectKind
{
get => (ObjectKind)Pointer->GameObject.ObjectKind;
set => Pointer->GameObject.ObjectKind = (byte)value;
}
public ByteString Utf8Name
=> new(Pointer->GameObject.Name);
public byte Job
=> Pointer->ClassJob;
public DrawObject DrawObject
=> (IntPtr)Pointer->GameObject.DrawObject;
public bool Valid
=> Pointer != null;
public int Index
=> Pointer->GameObject.ObjectIndex;
public uint ModelId
{
get => (uint)Pointer->ModelCharaId;
set => Pointer->ModelCharaId = (int)value;
}
public ushort UsedMountId
=> !IsHuman ? (ushort)0 : *(ushort*)((byte*)Pointer + 0x668);
public ushort CompanionId
=> ObjectKind == ObjectKind.Companion ? *(ushort*)((byte*)Pointer + 0x1AAC) : (ushort)0;
public Customize Customize
=> new(*(CustomizeData*)&Pointer->DrawData.CustomizeData);
public CharacterEquip Equip
=> new((CharacterArmor*)&Pointer->DrawData.Head);
public CharacterWeapon MainHand
{
get => *(CharacterWeapon*)&Pointer->DrawData.MainHandModel;
set => *(CharacterWeapon*)&Pointer->DrawData.MainHandModel = value;
}
public CharacterWeapon OffHand
{
get => *(CharacterWeapon*)&Pointer->DrawData.OffHandModel;
set => *(CharacterWeapon*)&Pointer->DrawData.OffHandModel = value;
}
public unsafe bool VisorEnabled
{
get => (*(byte*)(Address + Offsets.Character.VisorToggled) & Offsets.Character.Flags.IsVisorToggled) != 0;
set => *(byte*)(Address + Offsets.Character.VisorToggled) = (byte)(value
? *(byte*)(Address + Offsets.Character.VisorToggled) | Offsets.Character.Flags.IsVisorToggled
: *(byte*)(Address + Offsets.Character.VisorToggled) & ~Offsets.Character.Flags.IsVisorToggled);
}
public unsafe bool WeaponEnabled
{
get => (*(byte*)(Address + Offsets.Character.WeaponHidden1) & Offsets.Character.Flags.IsWeaponHidden1) == 0;
set
{
ref var w1 = ref *(byte*)(Address + Offsets.Character.WeaponHidden1);
ref var w2 = ref *(byte*)(Address + Offsets.Character.WeaponHidden2);
if (value)
{
w1 = (byte)(w1 & ~Offsets.Character.Flags.IsWeaponHidden1);
w2 = (byte)(w2 & ~Offsets.Character.Flags.IsWeaponHidden2);
}
else
{
w1 = (byte)(w1 | Offsets.Character.Flags.IsWeaponHidden1);
w2 = (byte)(w2 | Offsets.Character.Flags.IsWeaponHidden2);
}
}
}
public bool IsWet { get; set; }
public void SetModelId(int value)
{
if (Pointer != null)
Pointer->ModelCharaId = value;
}
public static implicit operator bool(Actor actor)
=> actor.Pointer != null;
public static bool operator true(Actor actor)
=> actor.Pointer != null;
public static bool operator false(Actor actor)
=> actor.Pointer == null;
public static bool operator !(Actor actor)
=> actor.Pointer == null;
public bool Equals(Actor other)
=> Pointer == other.Pointer;
public override bool Equals(object? obj)
=> obj is Actor other && Equals(other);
public override int GetHashCode()
=> ((ulong)Pointer).GetHashCode();
public static bool operator ==(Actor lhs, Actor rhs)
=> lhs.Pointer == rhs.Pointer;
public static bool operator !=(Actor lhs, Actor rhs)
=> lhs.Pointer != rhs.Pointer;
}

View file

@ -1,24 +1,34 @@
using Dalamud.Utility.Signatures;
using FFXIVClientStructs.FFXIV.Client.Graphics.Scene;
using Glamourer.Interop.Structs;
using Penumbra.GameData.Structs;
namespace Glamourer.Interop;
/// <summary>
/// Access the function the game uses to update customize data on the character screen.
/// Changes in Race, body type or Gender are probably ignored.
/// This operates on draw objects, not game objects.
/// </summary>
public unsafe class ChangeCustomizeService
{
public ChangeCustomizeService()
=> SignatureHelper.Initialise(this);
public delegate bool ChangeCustomizeDelegate(Human* human, byte* data, byte skipEquipment);
private delegate bool ChangeCustomizeDelegate(Human* human, byte* data, byte skipEquipment);
[Signature(Sigs.ChangeCustomize)]
private readonly ChangeCustomizeDelegate _changeCustomize = null!;
public bool UpdateCustomize(Actor actor, CustomizeData customize)
public bool UpdateCustomize(Model model, CustomizeData customize)
{
if (customize.Data == null || !actor.Valid || !actor.DrawObject.Valid)
if (!model.IsHuman)
return false;
return _changeCustomize(actor.DrawObject.Pointer, customize.Data, 1);
Item.Log.Verbose($"[ChangeCustomize] Invoked on 0x{model.Address:X} with {customize}.");
return _changeCustomize(model.AsHuman, customize.Data, 1);
}
public bool UpdateCustomize(Actor actor, CustomizeData customize)
=> UpdateCustomize(actor.Model, customize);
}

View file

@ -1,100 +0,0 @@
using System;
using FFXIVClientStructs.FFXIV.Client.Graphics.Scene;
using Glamourer.Customization;
using Penumbra.GameData.Structs;
using CustomizeData = Penumbra.GameData.Structs.CustomizeData;
namespace Glamourer.Interop;
public unsafe partial struct DrawObject : IEquatable<DrawObject>, IDesignable
{
public Human* Pointer;
public IntPtr Address
=> (IntPtr)Pointer;
public static implicit operator DrawObject(IntPtr? pointer)
=> new() { Pointer = (Human*)(pointer ?? IntPtr.Zero) };
public static implicit operator IntPtr(DrawObject drawObject)
=> drawObject.Pointer == null ? IntPtr.Zero : (IntPtr)drawObject.Pointer;
public bool Valid
=> Pointer != null;
public uint ModelId
=> 0;
public bool IsWet
=> false;
public uint Type
=> (*(delegate* unmanaged<Human*, uint>**)Pointer)[50](Pointer);
public Customize Customize
=> *(Customize*)Pointer->CustomizeData;
public CharacterEquip Equip
=> new((CharacterArmor*)Pointer->EquipSlotData);
public CharacterWeapon MainHand
{
get
{
var child = (byte*)Pointer->CharacterBase.DrawObject.Object.ChildObject;
if (child == null)
return CharacterWeapon.Empty;
return *(CharacterWeapon*)(child + 0x8F0);
}
}
public unsafe CharacterWeapon OffHand
{
get
{
var child = Pointer->CharacterBase.DrawObject.Object.ChildObject;
if (child == null)
return CharacterWeapon.Empty;
var sibling = (byte*)child->NextSiblingObject;
if (sibling == null)
return CharacterWeapon.Empty;
return *(CharacterWeapon*)(sibling + 0x8F0);
}
}
public unsafe bool VisorEnabled
=> (*(byte*)(Address + 0x90) & 0x40) != 0;
public unsafe bool WeaponEnabled
=> false;
public static implicit operator bool(DrawObject actor)
=> actor.Pointer != null;
public static bool operator true(DrawObject actor)
=> actor.Pointer != null;
public static bool operator false(DrawObject actor)
=> actor.Pointer == null;
public static bool operator !(DrawObject actor)
=> actor.Pointer == null;
public bool Equals(DrawObject other)
=> Pointer == other.Pointer;
public override bool Equals(object? obj)
=> obj is DrawObject other && Equals(other);
public override int GetHashCode()
=> unchecked((int)(long)Pointer);
public static bool operator ==(DrawObject lhs, DrawObject rhs)
=> lhs.Pointer == rhs.Pointer;
public static bool operator !=(DrawObject lhs, DrawObject rhs)
=> lhs.Pointer != rhs.Pointer;
}

View file

@ -1,17 +0,0 @@
using Glamourer.Customization;
using Penumbra.GameData.Structs;
namespace Glamourer.Interop;
public interface IDesignable
{
public bool Valid { get; }
public uint ModelId { get; }
public Customize Customize { get; }
public CharacterEquip Equip { get; }
public CharacterWeapon MainHand { get; }
public CharacterWeapon OffHand { get; }
public bool VisorEnabled { get; }
public bool WeaponEnabled { get; }
public bool IsWet { get; }
}

View file

@ -1,43 +0,0 @@
using System;
using System.Collections.Generic;
using Dalamud.Data;
using Dalamud.Hooking;
using Dalamud.Utility.Signatures;
using Glamourer.Structs;
namespace Glamourer.Interop;
public class JobService : IDisposable
{
public readonly IReadOnlyDictionary<byte, Job> Jobs;
public readonly IReadOnlyDictionary<ushort, JobGroup> JobGroups;
public event Action<Actor, Job>? JobChanged;
public JobService(DataManager gameData)
{
SignatureHelper.Initialise(this);
Jobs = GameData.Jobs(gameData);
JobGroups = GameData.JobGroups(gameData);
_changeJobHook.Enable();
}
public void Dispose()
{
_changeJobHook.Dispose();
}
private delegate void ChangeJobDelegate(nint data, uint job);
[Signature(Sigs.ChangeJob, DetourName = nameof(ChangeJobDetour))]
private readonly Hook<ChangeJobDelegate> _changeJobHook = null!;
private void ChangeJobDetour(nint data, uint jobIndex)
{
_changeJobHook.Original(data, jobIndex);
var actor = (Actor)(data - Offsets.Character.ClassJobContainer);
var job = Jobs[(byte)jobIndex];
Glamourer.Log.Excessive($"{actor} changed job to {job}");
JobChanged?.Invoke(actor, job);
}
}

View file

@ -1,160 +0,0 @@
using System;
using System.Collections;
using System.Collections.Generic;
using Dalamud.Game;
using Dalamud.Game.ClientState;
using Dalamud.Game.ClientState.Objects;
using Glamourer.Services;
using Penumbra.GameData.Actors;
namespace Glamourer.Interop;
public readonly struct ActorData
{
public readonly List<Actor> Objects;
public readonly string Label;
public bool Valid
=> Objects.Count > 0;
public ActorData(Actor actor, string label)
{
Objects = new List<Actor> { actor };
Label = label;
}
public static readonly ActorData Invalid = new(false);
private ActorData(bool _)
{
Objects = new List<Actor>(0);
Label = string.Empty;
}
}
public class ObjectManager : IReadOnlyDictionary<ActorIdentifier, ActorData>
{
private readonly Framework _framework;
private readonly ClientState _clientState;
private readonly ObjectTable _objects;
private readonly ActorService _actors;
public ObjectManager(Framework framework, ClientState clientState, ObjectTable objects, ActorService actors)
{
_framework = framework;
_clientState = clientState;
_objects = objects;
_actors = actors;
}
public DateTime LastUpdate { get; private set; }
public bool IsInGPose { get; private set; }
public ushort World { get; private set; }
private readonly Dictionary<ActorIdentifier, ActorData> _identifiers = new(200);
private void HandleIdentifier(ActorIdentifier identifier, Actor character)
{
if (!character.DrawObject || !identifier.IsValid)
return;
if (!_identifiers.TryGetValue(identifier, out var data))
{
data = new ActorData(character, identifier.ToString());
_identifiers[identifier] = data;
}
else
{
data.Objects.Add(character);
}
}
public void Update()
{
var lastUpdate = _framework.LastUpdate;
if (lastUpdate <= LastUpdate)
return;
LastUpdate = lastUpdate;
World = (ushort)(_clientState.LocalPlayer?.CurrentWorld.Id ?? 0u);
_identifiers.Clear();
for (var i = 0; i < (int)ScreenActor.CutsceneStart; ++i)
{
Actor character = _objects.GetObjectAddress(i);
if (character.Identifier(_actors.AwaitedService, out var identifier))
HandleIdentifier(identifier, character);
}
for (var i = (int)ScreenActor.CutsceneStart; i < (int)ScreenActor.CutsceneEnd; ++i)
{
Actor character = _objects.GetObjectAddress(i);
if (!character.Identifier(_actors.AwaitedService, out var identifier))
break;
HandleIdentifier(identifier, character);
}
void AddSpecial(ScreenActor idx, string label)
{
Actor actor = _objects.GetObjectAddress((int)idx);
if (actor.Identifier(_actors.AwaitedService, out var ident))
{
var data = new ActorData(actor, label);
_identifiers.Add(ident, data);
}
}
AddSpecial(ScreenActor.CharacterScreen, "Character Screen Actor");
AddSpecial(ScreenActor.ExamineScreen, "Examine Screen Actor");
AddSpecial(ScreenActor.FittingRoom, "Fitting Room Actor");
AddSpecial(ScreenActor.DyePreview, "Dye Preview Actor");
AddSpecial(ScreenActor.Portrait, "Portrait Actor");
AddSpecial(ScreenActor.Card6, "Card Actor 6");
AddSpecial(ScreenActor.Card7, "Card Actor 7");
AddSpecial(ScreenActor.Card8, "Card Actor 8");
for (var i = (int)ScreenActor.ScreenEnd; i < _objects.Length; ++i)
{
Actor character = _objects.GetObjectAddress(i);
if (character.Identifier(_actors.AwaitedService, out var identifier))
HandleIdentifier(identifier, character);
}
var gPose = GPosePlayer;
IsInGPose = gPose && gPose.Utf8Name.Length > 0;
}
public Actor GPosePlayer
=> _objects.GetObjectAddress((int)ScreenActor.GPosePlayer);
public Actor Player
=> _objects.GetObjectAddress(0);
public IEnumerator<KeyValuePair<ActorIdentifier, ActorData>> GetEnumerator()
=> _identifiers.GetEnumerator();
IEnumerator IEnumerable.GetEnumerator()
=> GetEnumerator();
public int Count
=> _identifiers.Count;
public bool ContainsKey(ActorIdentifier key)
=> _identifiers.ContainsKey(key);
public bool TryGetValue(ActorIdentifier key, out ActorData value)
=> _identifiers.TryGetValue(key, out value);
public ActorData this[ActorIdentifier key]
=> _identifiers[key];
public IEnumerable<ActorIdentifier> Keys
=> _identifiers.Keys;
public IEnumerable<ActorData> Values
=> _identifiers.Values;
}

View file

@ -0,0 +1,142 @@
using System;
using Dalamud.Logging;
using Dalamud.Plugin;
using Glamourer.Interop.Structs;
using Penumbra.Api;
using Penumbra.Api.Enums;
using Penumbra.Api.Helpers;
namespace Glamourer.Interop.Penumbra;
public unsafe class PenumbraService : IDisposable
{
public const int RequiredPenumbraBreakingVersion = 4;
public const int RequiredPenumbraFeatureVersion = 15;
private readonly DalamudPluginInterface _pluginInterface;
private readonly EventSubscriber<ChangedItemType, uint> _tooltipSubscriber;
private readonly EventSubscriber<MouseButton, ChangedItemType, uint> _clickSubscriber;
private readonly EventSubscriber<nint, string, nint, nint, nint> _creatingCharacterBase;
private readonly EventSubscriber<nint, string, nint> _createdCharacterBase;
private ActionSubscriber<int, RedrawType> _redrawSubscriber;
private FuncSubscriber<nint, (nint, string)> _drawObjectInfo;
private FuncSubscriber<int, int> _cutsceneParent;
private readonly EventSubscriber _initializedEvent;
private readonly EventSubscriber _disposedEvent;
public bool Available { get; private set; }
public PenumbraService(DalamudPluginInterface pi)
{
_pluginInterface = pi;
_initializedEvent = Ipc.Initialized.Subscriber(pi, Reattach);
_disposedEvent = Ipc.Disposed.Subscriber(pi, Unattach);
_tooltipSubscriber = Ipc.ChangedItemTooltip.Subscriber(pi);
_clickSubscriber = Ipc.ChangedItemClick.Subscriber(pi);
_createdCharacterBase = Ipc.CreatedCharacterBase.Subscriber(pi);
_creatingCharacterBase = Ipc.CreatingCharacterBase.Subscriber(pi);
Reattach();
}
public event Action<MouseButton, ChangedItemType, uint> Click
{
add => _clickSubscriber.Event += value;
remove => _clickSubscriber.Event -= value;
}
public event Action<ChangedItemType, uint> Tooltip
{
add => _tooltipSubscriber.Event += value;
remove => _tooltipSubscriber.Event -= value;
}
public event Action<nint, string, nint, nint, nint> CreatingCharacterBase
{
add => _creatingCharacterBase.Event += value;
remove => _creatingCharacterBase.Event -= value;
}
public event Action<nint, string, nint> CreatedCharacterBase
{
add => _createdCharacterBase.Event += value;
remove => _createdCharacterBase.Event -= value;
}
/// <summary> Obtain the game object corresponding to a draw object. </summary>
public Actor GameObjectFromDrawObject(Model drawObject)
=> Available ? _drawObjectInfo.Invoke(drawObject.Address).Item1 : Actor.Null;
/// <summary> Obtain the parent of a cutscene actor if it is known. </summary>
public int CutsceneParent(int idx)
=> Available ? _cutsceneParent.Invoke(idx) : -1;
/// <summary> Try to redraw the given actor. </summary>
public void RedrawObject(Actor actor, RedrawType settings)
{
if (!actor || !Available)
return;
try
{
_redrawSubscriber.Invoke(actor.AsObject->ObjectIndex, settings);
}
catch (Exception e)
{
PluginLog.Debug($"Failure redrawing object:\n{e}");
}
}
/// <summary> Reattach to the currently running Penumbra IPC provider. Unattaches before if necessary. </summary>
public void Reattach()
{
try
{
Unattach();
var (breaking, feature) = Ipc.ApiVersions.Subscriber(_pluginInterface).Invoke();
if (breaking != RequiredPenumbraBreakingVersion || feature < RequiredPenumbraFeatureVersion)
throw new Exception(
$"Invalid Version {breaking}.{feature:D4}, required major Version {RequiredPenumbraBreakingVersion} with feature greater or equal to {RequiredPenumbraFeatureVersion}.");
_tooltipSubscriber.Enable();
_clickSubscriber.Enable();
_creatingCharacterBase.Enable();
_createdCharacterBase.Enable();
_drawObjectInfo = Ipc.GetDrawObjectInfo.Subscriber(_pluginInterface);
_cutsceneParent = Ipc.GetCutsceneParentIndex.Subscriber(_pluginInterface);
_redrawSubscriber = Ipc.RedrawObjectByIndex.Subscriber(_pluginInterface);
Available = true;
Item.Log.Debug("Glamourer attached to Penumbra.");
}
catch (Exception e)
{
Item.Log.Debug($"Could not attach to Penumbra:\n{e}");
}
}
/// <summary> Unattach from the currently running Penumbra IPC provider. </summary>
public void Unattach()
{
_tooltipSubscriber.Disable();
_clickSubscriber.Disable();
_creatingCharacterBase.Disable();
_createdCharacterBase.Disable();
if (Available)
{
Available = false;
Item.Log.Debug("Glamourer detached from Penumbra.");
}
}
public void Dispose()
{
Unattach();
_tooltipSubscriber.Dispose();
_clickSubscriber.Dispose();
_creatingCharacterBase.Dispose();
_createdCharacterBase.Dispose();
_initializedEvent.Dispose();
_disposedEvent.Dispose();
}
}

View file

@ -1,93 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Dalamud.Game.ClientState;
using Dalamud.Logging;
using Dalamud.Utility.Signatures;
using Glamourer.Api;
using Glamourer.Customization;
using Glamourer.Services;
using Glamourer.State;
using Penumbra.GameData.Enums;
using Penumbra.GameData.Structs;
using CustomizeData = Penumbra.GameData.Structs.CustomizeData;
namespace Glamourer.Interop;
public unsafe partial class RedrawManager : IDisposable
{
private readonly ItemManager _items;
private readonly ActorService _actors;
private readonly FixedDesignManager _fixedDesignManager;
private readonly ActiveDesign.Manager _stateManager;
private readonly PenumbraAttach _penumbra;
private readonly WeaponService _weapons;
public RedrawManager(FixedDesignManager fixedDesignManager, ActiveDesign.Manager stateManager, ItemManager items, ActorService actors,
PenumbraAttach penumbra, WeaponService weapons)
{
SignatureHelper.Initialise(this);
_fixedDesignManager = fixedDesignManager;
_stateManager = stateManager;
_items = items;
_actors = actors;
_penumbra = penumbra;
_weapons = weapons;
_penumbra.CreatingCharacterBase += OnCharacterRedraw;
_penumbra.CreatedCharacterBase += OnCharacterRedrawFinished;
}
public void Dispose()
{
}
private void OnCharacterRedraw(Actor actor, uint* modelId, Customize customize, CharacterEquip equip)
{
// Do not apply anything if the game object model id does not correspond to the draw object model id.
// This is the case if the actor is transformed to a different creature.
if (actor.ModelId != *modelId)
return;
// Check if we have a current design in use, or if not if the actor has a fixed design.
var identifier = actor.GetIdentifier(_actors.AwaitedService);
if (!_stateManager.TryGetValue(identifier, out var save))
return;
// Compare game object customize data against draw object customize data for transformations.
// Apply customization if they correspond and there is customization to apply.
var gameObjectCustomize = new Customize(*(CustomizeData*)&actor.Pointer->DrawData.CustomizeData);
if (gameObjectCustomize.Equals(customize))
customize.Load(save.ModelData.Customize);
// Compare game object equip data against draw object equip data for transformations.
// Apply each piece of equip that should be applied if they correspond.
var gameObjectEquip = new CharacterEquip((CharacterArmor*)&actor.Pointer->DrawData.Head);
if (gameObjectEquip.Equals(equip))
{
foreach (var slot in EquipSlotExtensions.EqdpSlots)
{
(_, equip[slot]) =
_items.ResolveRestrictedGear(save.ModelData.Armor(slot), slot, customize.Race, customize.Gender);
}
}
}
private void OnCharacterRedraw(IntPtr gameObject, string collection, IntPtr modelId, IntPtr customize, IntPtr equipData)
{
try
{
OnCharacterRedraw(gameObject, (uint*)modelId, new Customize(*(CustomizeData*)customize),
new CharacterEquip((CharacterArmor*)equipData));
}
catch (Exception e)
{
PluginLog.Error($"Error on new draw object creation:\n{e}");
}
}
private static void OnCharacterRedrawFinished(IntPtr gameObject, string collection, IntPtr drawObject)
{
//SetVisor((Human*)drawObject, true);
}
}

View file

@ -0,0 +1,97 @@
using Penumbra.GameData.Actors;
using System;
using FFXIVClientStructs.FFXIV.Client.Game.Character;
using FFXIVClientStructs.FFXIV.Client.Game.Object;
using Penumbra.GameData.Enums;
using Penumbra.GameData.Structs;
namespace Glamourer.Interop.Structs;
public readonly unsafe struct Actor : IEquatable<Actor>
{
private Actor(nint address)
=> Address = address;
public static readonly Actor Null = new(nint.Zero);
public readonly nint Address;
public GameObject* AsObject
=> (GameObject*)Address;
public Character* AsCharacter
=> (Character*)Address;
public bool Valid
=> Address != nint.Zero;
public bool IsCharacter
=> Valid && AsObject->IsCharacter();
public static implicit operator Actor(nint? pointer)
=> new(pointer ?? nint.Zero);
public static implicit operator Actor(GameObject* pointer)
=> new((nint)pointer);
public static implicit operator Actor(Character* pointer)
=> new((nint)pointer);
public static implicit operator nint(Actor actor)
=> actor.Address;
public ActorIdentifier GetIdentifier(ActorManager actors)
=> actors.FromObject(AsObject, out _, true, true, false);
public bool Identifier(ActorManager actors, out ActorIdentifier ident)
{
if (Valid)
{
ident = GetIdentifier(actors);
return ident.IsValid;
}
ident = ActorIdentifier.Invalid;
return false;
}
public Model Model
=> Valid ? AsObject->DrawObject : null;
public static implicit operator bool(Actor actor)
=> actor.Address != nint.Zero;
public static bool operator true(Actor actor)
=> actor.Address != nint.Zero;
public static bool operator false(Actor actor)
=> actor.Address == nint.Zero;
public static bool operator !(Actor actor)
=> actor.Address == nint.Zero;
public bool Equals(Actor other)
=> Address == other.Address;
public override bool Equals(object? obj)
=> obj is Actor other && Equals(other);
public override int GetHashCode()
=> Address.GetHashCode();
public static bool operator ==(Actor lhs, Actor rhs)
=> lhs.Address == rhs.Address;
public static bool operator !=(Actor lhs, Actor rhs)
=> lhs.Address != rhs.Address;
/// <summary> Only valid for characters. </summary>
public CharacterArmor GetArmor(EquipSlot slot)
=> ((CharacterArmor*)&AsCharacter->DrawData.Head)[slot.ToIndex()];
public CharacterWeapon GetMainhand()
=> *(CharacterWeapon*)&AsCharacter->DrawData.MainHandModel;
public CharacterWeapon GetOffhand()
=> *(CharacterWeapon*)&AsCharacter->DrawData.OffHandModel;
}

View file

@ -0,0 +1,26 @@
using System.Collections.Generic;
namespace Glamourer.Interop.Structs;
public readonly struct ActorData
{
public readonly List<Actor> Objects;
public readonly string Label;
public bool Valid
=> Objects.Count > 0;
public ActorData(Actor actor, string label)
{
Objects = new List<Actor> { actor };
Label = label;
}
public static readonly ActorData Invalid = new(false);
private ActorData(bool _)
{
Objects = new List<Actor>(0);
Label = string.Empty;
}
}

View file

@ -0,0 +1,92 @@
using System;
using FFXIVClientStructs.FFXIV.Client.Graphics.Scene;
using Penumbra.GameData.Enums;
using Penumbra.GameData.Structs;
using ObjectType = FFXIVClientStructs.FFXIV.Client.Graphics.Scene.ObjectType;
namespace Glamourer.Interop.Structs;
public readonly unsafe struct Model : IEquatable<Model>
{
private Model(nint address)
=> Address = address;
public readonly nint Address;
public static readonly Model Null = new(0);
public DrawObject* AsDrawObject
=> (DrawObject*)Address;
public CharacterBase* AsCharacterBase
=> (CharacterBase*)Address;
public Human* AsHuman
=> (Human*)Address;
public static implicit operator Model(nint? pointer)
=> new(pointer ?? nint.Zero);
public static implicit operator Model(DrawObject* pointer)
=> new((nint)pointer);
public static implicit operator Model(Human* pointer)
=> new((nint)pointer);
public static implicit operator Model(CharacterBase* pointer)
=> new((nint)pointer);
public static implicit operator nint(Model model)
=> model.Address;
public bool Valid
=> Address != nint.Zero;
public bool IsCharacterBase
=> Valid && AsDrawObject->Object.GetObjectType() == ObjectType.CharacterBase;
public bool IsHuman
=> IsCharacterBase && AsCharacterBase->GetModelType() == CharacterBase.ModelType.Human;
public static implicit operator bool(Model actor)
=> actor.Address != nint.Zero;
public static bool operator true(Model actor)
=> actor.Address != nint.Zero;
public static bool operator false(Model actor)
=> actor.Address == nint.Zero;
public static bool operator !(Model actor)
=> actor.Address == nint.Zero;
public bool Equals(Model other)
=> Address == other.Address;
public override bool Equals(object? obj)
=> obj is Model other && Equals(other);
public override int GetHashCode()
=> Address.GetHashCode();
public static bool operator ==(Model lhs, Model rhs)
=> lhs.Address == rhs.Address;
public static bool operator !=(Model lhs, Model rhs)
=> lhs.Address != rhs.Address;
/// <summary> Only valid for humans. </summary>
public CharacterArmor GetArmor(EquipSlot slot)
=> ((CharacterArmor*)AsHuman->EquipSlotData)[slot.ToIndex()];
public CharacterWeapon GetMainhand()
{
var weapon = AsDrawObject->Object.ChildObject;
if (weapon == null)
return CharacterWeapon.Empty;
weapon
}
public CharacterWeapon GetOffhand()
=> *(CharacterWeapon*)&AsCharacter->DrawData.OffHandModel;
}

View file

@ -1,7 +1,8 @@
using System;
using System.Linq;
using Dalamud.Hooking;
using Dalamud.Utility.Signatures;
using Glamourer.Events;
using Glamourer.Interop.Structs;
using Penumbra.GameData.Enums;
using Penumbra.GameData.Structs;
@ -9,8 +10,11 @@ namespace Glamourer.Interop;
public unsafe class UpdateSlotService : IDisposable
{
public UpdateSlotService()
public readonly UpdatedSlot Event;
public UpdateSlotService(UpdatedSlot updatedSlot)
{
Event = updatedSlot;
SignatureHelper.Initialise(this);
_flagSlotForUpdateHook.Enable();
}
@ -19,59 +23,41 @@ public unsafe class UpdateSlotService : IDisposable
=> _flagSlotForUpdateHook.Dispose();
private delegate ulong FlagSlotForUpdateDelegateIntern(nint drawObject, uint slot, CharacterArmor* data);
public delegate void FlagSlotForUpdateDelegate(DrawObject drawObject, EquipSlot slot, ref CharacterArmor item);
// This gets called when one of the ten equip items of an existing draw object gets changed.
[Signature(Sigs.FlagSlotForUpdate, DetourName = nameof(FlagSlotForUpdateDetour))]
private readonly Hook<FlagSlotForUpdateDelegateIntern> _flagSlotForUpdateHook = null!;
public event FlagSlotForUpdateDelegate? EquipUpdate;
public ulong FlagSlotForUpdateInterop(DrawObject drawObject, EquipSlot slot, CharacterArmor armor)
=> _flagSlotForUpdateHook.Original(drawObject.Address, slot.ToIndex(), &armor);
public void UpdateSlot(DrawObject drawObject, EquipSlot slot, CharacterArmor data)
public void UpdateSlot(Model drawObject, EquipSlot slot, CharacterArmor data)
{
InvokeFlagSlotEvent(drawObject, slot, ref data);
if (!drawObject.IsCharacterBase)
return;
FlagSlotForUpdateInterop(drawObject, slot, data);
}
public void UpdateStain(DrawObject drawObject, EquipSlot slot, StainId stain)
public void UpdateArmor(Model drawObject, EquipSlot slot, CharacterArmor data)
{
var armor = drawObject.Equip[slot] with { Stain = stain };
UpdateSlot(drawObject, slot, armor);
if (!drawObject.IsCharacterBase)
return;
FlagSlotForUpdateInterop(drawObject, slot, data.With(drawObject.GetArmor(slot).Stain));
}
public void UpdateStain(Model drawObject, EquipSlot slot, StainId stain)
{
if (!drawObject.IsHuman)
return;
FlagSlotForUpdateInterop(drawObject, slot, drawObject.GetArmor(slot).With(stain));
}
private ulong FlagSlotForUpdateDetour(nint drawObject, uint slotIdx, CharacterArmor* data)
{
var slot = slotIdx.ToEquipSlot();
InvokeFlagSlotEvent(drawObject, slot, ref *data);
return _flagSlotForUpdateHook.Original(drawObject, slotIdx, data);
var slot = slotIdx.ToEquipSlot();
var returnValue = ulong.MaxValue;
Event.Invoke(drawObject, slot, ref *data, ref returnValue);
return returnValue == ulong.MaxValue ? _flagSlotForUpdateHook.Original(drawObject, slotIdx, data) : returnValue;
}
private void InvokeFlagSlotEvent(DrawObject drawObject, EquipSlot slot, ref CharacterArmor armor)
{
if (EquipUpdate == null)
{
Glamourer.Log.Excessive(
$"{slot} updated on 0x{drawObject.Address:X} to {armor.Set.Value}-{armor.Variant} with stain {armor.Stain.Value}.");
return;
}
var iv = armor;
foreach (var del in EquipUpdate.GetInvocationList().OfType<FlagSlotForUpdateDelegate>())
{
try
{
del(drawObject, slot, ref armor);
}
catch (Exception ex)
{
Glamourer.Log.Error($"Could not invoke {nameof(EquipUpdate)} Subscriber:\n{ex}");
}
}
Glamourer.Log.Excessive(
$"{slot} updated on 0x{drawObject.Address:X} to {armor.Set.Value}-{armor.Variant} with stain {armor.Stain.Value}, initial armor was {iv.Set.Value}-{iv.Variant} with stain {iv.Stain.Value}.");
}
private ulong FlagSlotForUpdateInterop(Model drawObject, EquipSlot slot, CharacterArmor armor)
=> _flagSlotForUpdateHook.Original(drawObject.Address, slot.ToIndex(), &armor);
}

View file

@ -1,16 +1,19 @@
using System;
using System.Linq;
using System.Runtime.CompilerServices;
using Dalamud.Hooking;
using Dalamud.Utility.Signatures;
using FFXIVClientStructs.FFXIV.Client.Graphics.Scene;
using Penumbra.GameData.Structs;
using Glamourer.Events;
using Glamourer.Interop.Structs;
namespace Glamourer.Interop;
public class VisorService : IDisposable
{
public VisorService()
public readonly VisorStateChanged Event;
public VisorService(VisorStateChanged visorStateChanged)
{
Event = visorStateChanged;
SignatureHelper.Initialise(this);
_setupVisorHook.Enable();
}
@ -18,61 +21,67 @@ public class VisorService : IDisposable
public void Dispose()
=> _setupVisorHook.Dispose();
public static unsafe bool GetVisorState(nint humanPtr)
/// <summary> Obtain the current state of the Visor for the given draw object (true: toggled). </summary>
public unsafe bool GetVisorState(Model characterBase)
{
if (humanPtr == IntPtr.Zero)
if (!characterBase.IsCharacterBase)
return false;
var data = (Human*)humanPtr;
var flags = &data->CharacterBase.UnkFlags_01;
return (*flags & Offsets.DrawObjectVisorStateFlag) != 0;
// TODO: use client structs.
return (characterBase.AsCharacterBase->UnkFlags_01 & Offsets.DrawObjectVisorStateFlag) != 0;
}
public unsafe void SetVisorState(nint humanPtr, bool on)
/// <summary> Manually set the state of the Visor for the given draw object. </summary>
/// <param name="human"> The draw object. </param>
/// <param name="on"> The desired state (true: toggled). </param>
/// <returns> Whether the state was changed. </returns>
public unsafe bool SetVisorState(Model human, bool on)
{
if (humanPtr == IntPtr.Zero)
return;
if (!human.IsHuman)
return false;
var data = (Human*)humanPtr;
_setupVisorHook.Original(humanPtr, (ushort) data->HeadSetID, on);
var oldState = GetVisorState(human);
Item.Log.Verbose($"[SetVisorState] Invoked manually on 0x{human.Address:X} switching from {oldState} to {on}.");
if (oldState == on)
return false;
SetupVisorHook(human, (ushort)human.AsHuman->HeadSetID, on);
return true;
}
private delegate void UpdateVisorDelegateInternal(nint humanPtr, ushort modelId, bool on);
public delegate void UpdateVisorDelegate(DrawObject human, SetId modelId, ref bool on);
[Signature(Penumbra.GameData.Sigs.SetupVisor, DetourName = nameof(SetupVisorDetour))]
[Signature(global::Penumbra.GameData.Sigs.SetupVisor, DetourName = nameof(SetupVisorDetour))]
private readonly Hook<UpdateVisorDelegateInternal> _setupVisorHook = null!;
public event UpdateVisorDelegate? VisorUpdate;
private void SetupVisorDetour(nint humanPtr, ushort modelId, bool on)
private void SetupVisorDetour(nint human, ushort modelId, bool on)
{
InvokeVisorEvent(humanPtr, modelId, ref on);
_setupVisorHook.Original(humanPtr, modelId, on);
var callOriginal = true;
var originalOn = on;
// Invoke an event that can change the requested value
// and also control whether the function should be called at all.
Event.Invoke(human, ref on, ref callOriginal);
Item.Log.Excessive(
$"[SetVisorState] Invoked from game on 0x{human:X} switching to {on} (original {originalOn}, call original {callOriginal}).");
if (callOriginal)
SetupVisorHook(human, modelId, on);
}
private void InvokeVisorEvent(DrawObject drawObject, SetId modelId, ref bool on)
/// <summary>
/// The SetupVisor function does not set the visor state for the draw object itself,
/// it only sets the "visor is changing" state to false.
/// So we wrap a manual change of that flag with the function call.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
private unsafe void SetupVisorHook(Model human, ushort modelId, bool on)
{
if (VisorUpdate == null)
{
Glamourer.Log.Excessive($"Visor setup on 0x{drawObject.Address:X} with {modelId.Value}, setting to {on}.");
return;
}
var initialValue = on;
foreach (var del in VisorUpdate.GetInvocationList().OfType<UpdateVisorDelegate>())
{
try
{
del(drawObject, modelId, ref on);
}
catch (Exception ex)
{
Glamourer.Log.Error($"Could not invoke {nameof(VisorUpdate)} Subscriber:\n{ex}");
}
}
Glamourer.Log.Excessive(
$"Visor setup on 0x{drawObject.Address:X} with {modelId.Value}, setting to {on}, initial call was {initialValue}.");
// TODO: use client structs.
human.AsCharacterBase->UnkFlags_01 = (byte)(on
? human.AsCharacterBase->UnkFlags_01 | Offsets.DrawObjectVisorStateFlag
: human.AsCharacterBase->UnkFlags_01 & ~Offsets.DrawObjectVisorStateFlag);
_setupVisorHook.Original(human.Address, modelId, on);
}
}

View file

@ -1,8 +1,8 @@
using System;
using System.Runtime.InteropServices;
using Dalamud.Hooking;
using Dalamud.Utility.Signatures;
using FFXIVClientStructs.FFXIV.Client.Game.Character;
using Glamourer.Interop.Structs;
using Penumbra.GameData.Enums;
using Penumbra.GameData.Structs;
@ -13,6 +13,7 @@ public unsafe class WeaponService : IDisposable
public WeaponService()
{
SignatureHelper.Initialise(this);
_loadWeaponHook = Hook<LoadWeaponDelegate>.FromAddress((nint) DrawDataContainer.MemberFunctionPointers.LoadWeapon, LoadWeaponDetour);
_loadWeaponHook.Enable();
}
@ -21,68 +22,18 @@ public unsafe class WeaponService : IDisposable
_loadWeaponHook.Dispose();
}
public static readonly int CharacterWeaponOffset = (int)Marshal.OffsetOf<Character>("DrawData");
private delegate void LoadWeaponDelegate(DrawDataContainer* drawData, uint slot, ulong weapon, byte redrawOnEquality, byte unk2, byte skipGameObject, byte unk4);
public delegate void LoadWeaponDelegate(nint offsetCharacter, uint slot, ulong weapon, byte redrawOnEquality, byte unk2,
byte skipGameObject,
byte unk4);
private readonly Hook<LoadWeaponDelegate> _loadWeaponHook;
// Weapons for a specific character are reloaded with this function.
// The first argument is a pointer to the game object but shifted a bit inside.
// slot is 0 for main hand, 1 for offhand, 2 for unknown (always called with empty data.
// weapon argument is the new weapon data.
// redrawOnEquality controls whether the game does anything if the new weapon is identical to the old one.
// skipGameObject seems to control whether the new weapons are written to the game object or just influence the draw object. (1 = skip, 0 = change)
// unk4 seemed to be the same as unk1.
[Signature(Penumbra.GameData.Sigs.WeaponReload, DetourName = nameof(LoadWeaponDetour))]
private readonly Hook<LoadWeaponDelegate> _loadWeaponHook = null!;
private void LoadWeaponDetour(nint characterOffset, uint slot, ulong weapon, byte redrawOnEquality, byte unk2, byte skipGameObject,
private void LoadWeaponDetour(DrawDataContainer* drawData, uint slot, ulong weapon, byte redrawOnEquality, byte unk2, byte skipGameObject,
byte unk4)
{
//var oldWeapon = weapon;
//var character = (Actor)(characterOffset - CharacterWeaponOffset);
//try
//{
// var identifier = character.GetIdentifier(_actors.AwaitedService);
// if (_fixedDesignManager.TryGetDesign(identifier, out var save))
// {
// PluginLog.Information($"Loaded weapon from fixed design for {identifier}.");
// weapon = slot switch
// {
// 0 => save.WeaponMain.Model.Value,
// 1 => save.WeaponOff.Model.Value,
// _ => weapon,
// };
// }
// else if (redrawOnEquality == 1 && _stateManager.TryGetValue(identifier, out var save2))
// {
// PluginLog.Information($"Loaded weapon from current design for {identifier}.");
// //switch (slot)
// //{
// // case 0:
// // save2.MainHand = new CharacterWeapon(weapon);
// // break;
// // case 1:
// // save2.Data.OffHand = new CharacterWeapon(weapon);
// // break;
// //}
// }
//}
//catch (Exception e)
//{
// PluginLog.Error($"Error on loading new weapon:\n{e}");
//}
var actor = (Actor) (nint)drawData->Unk8;
// First call the regular function.
_loadWeaponHook.Original(characterOffset, slot, weapon, redrawOnEquality, unk2, skipGameObject, unk4);
Glamourer.Log.Excessive($"Weapon reloaded for {(Actor)(characterOffset - CharacterWeaponOffset)} with attributes {slot} {weapon:X14}, {redrawOnEquality}, {unk2}, {skipGameObject}, {unk4}");
// // If something changed the weapon, call it again with the actual change, not forcing redraws and skipping applying it to the game object.
// if (oldWeapon != weapon)
// _loadWeaponHook.Original(characterOffset, slot, weapon, 0 /* redraw */, unk2, 1 /* skip */, unk4);
// // If we're not actively changing the offhand and the game object has no offhand, redraw an empty offhand to fix animation problems.
// else if (slot != 1 && character.OffHand.Value == 0)
// _loadWeaponHook.Original(characterOffset, 1, 0, 1 /* redraw */, unk2, 1 /* skip */, unk4);
_loadWeaponHook.Original(drawData, slot, weapon, redrawOnEquality, unk2, skipGameObject, unk4);
Item.Log.Information($"Weapon reloaded for 0x{actor.Address:X} with attributes {slot} {weapon:X14}, {redrawOnEquality}, {unk2}, {skipGameObject}, {unk4}");
}
// Load a specific weapon for a character by its data and slot.
@ -91,14 +42,14 @@ public unsafe class WeaponService : IDisposable
switch (slot)
{
case EquipSlot.MainHand:
LoadWeaponDetour(character.Address + CharacterWeaponOffset, 0, weapon.Value, 0, 0, 1, 0);
LoadWeaponDetour(&character.AsCharacter->DrawData, 0, weapon.Value, 0, 0, 1, 0);
return;
case EquipSlot.OffHand:
LoadWeaponDetour(character.Address + CharacterWeaponOffset, 1, weapon.Value, 0, 0, 1, 0);
LoadWeaponDetour(&character.AsCharacter->DrawData, 1, weapon.Value, 0, 0, 1, 0);
return;
case EquipSlot.BothHand:
LoadWeaponDetour(character.Address + CharacterWeaponOffset, 0, weapon.Value, 0, 0, 1, 0);
LoadWeaponDetour(character.Address + CharacterWeaponOffset, 1, CharacterWeapon.Empty.Value, 0, 0, 1, 0);
LoadWeaponDetour(&character.AsCharacter->DrawData, 0, weapon.Value, 0, 0, 1, 0);
LoadWeaponDetour(&character.AsCharacter->DrawData, 1, CharacterWeapon.Empty.Value, 0, 0, 1, 0);
return;
// function can also be called with '2', but does not seem to ever be.
}
@ -107,14 +58,14 @@ public unsafe class WeaponService : IDisposable
// Load specific Main- and Offhand weapons.
public void LoadWeapon(Actor character, CharacterWeapon main, CharacterWeapon off)
{
LoadWeaponDetour(character.Address + CharacterWeaponOffset, 0, main.Value, 1, 0, 1, 0);
LoadWeaponDetour(character.Address + CharacterWeaponOffset, 1, off.Value, 1, 0, 1, 0);
LoadWeaponDetour(&character.AsCharacter->DrawData, 0, main.Value, 1, 0, 1, 0);
LoadWeaponDetour(&character.AsCharacter->DrawData, 1, off.Value, 1, 0, 1, 0);
}
public void LoadStain(Actor character, EquipSlot slot, StainId stain)
{
var weapon = slot == EquipSlot.OffHand ? character.OffHand : character.MainHand;
weapon.Stain = stain;
var value = slot == EquipSlot.OffHand ? character.AsCharacter->DrawData.OffHandModel : character.AsCharacter->DrawData.MainHandModel;
var weapon = new CharacterWeapon(value.Value) { Stain = stain.Value };
LoadWeapon(character, slot, weapon);
}
}