This commit is contained in:
Ottermandias 2022-08-23 18:06:28 +02:00
parent cb2e2f0128
commit 941bba1518
39 changed files with 2569 additions and 1579 deletions

View file

@ -0,0 +1,350 @@
using System;
using Dalamud.Game.ClientState.Objects.Enums;
using Dalamud.Utility;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using Penumbra.GameData.ByteString;
namespace Glamourer.Interop;
public unsafe partial struct Actor
{
public interface IIdentifier : IEquatable<IIdentifier>
{
Utf8String Name { get; }
public bool IsValid { get; }
public IIdentifier CreatePermanent();
public static readonly InvalidIdentifier Invalid = new();
public void ToJson(JsonTextWriter j);
public static IIdentifier? FromJson(JObject j)
{
switch (j["Type"]?.Value<string>() ?? string.Empty)
{
case nameof(PlayerIdentifier):
{
var name = j[nameof(Name)]?.Value<string>();
if (name.IsNullOrEmpty())
return null;
var serverId = j[nameof(PlayerIdentifier.HomeWorld)]?.Value<ushort>() ?? ushort.MaxValue;
return new PlayerIdentifier(Utf8String.FromStringUnsafe(name, false), serverId);
}
case nameof(SpecialIdentifier):
{
var index = j[nameof(SpecialIdentifier.Index)]?.Value<ushort>() ?? ushort.MaxValue;
return new SpecialIdentifier(index);
}
case nameof(OwnedIdentifier):
{
var name = j[nameof(Name)]?.Value<string>();
if (name.IsNullOrEmpty())
return null;
var ownerName = j[nameof(OwnedIdentifier.OwnerName)]?.Value<string>();
if (ownerName.IsNullOrEmpty())
return null;
var ownerHomeWorld = j[nameof(OwnedIdentifier.OwnerHomeWorld)]?.Value<ushort>() ?? ushort.MaxValue;
var dataId = j[nameof(OwnedIdentifier.DataId)]?.Value<ushort>() ?? ushort.MaxValue;
var kind = j[nameof(OwnedIdentifier.Kind)]?.Value<ObjectKind>() ?? ObjectKind.Player;
return new OwnedIdentifier(Utf8String.FromStringUnsafe(name, false), Utf8String.FromStringUnsafe(ownerName, false),
ownerHomeWorld, dataId, kind);
}
case nameof(NpcIdentifier):
{
var name = j[nameof(Name)]?.Value<string>();
if (name.IsNullOrEmpty())
return null;
var dataId = j[nameof(NpcIdentifier.DataId)]?.Value<uint>() ?? uint.MaxValue;
return new NpcIdentifier(Utf8String.FromStringUnsafe(name, false), ushort.MaxValue, dataId);
}
default: return null;
}
}
}
public class InvalidIdentifier : IIdentifier
{
public Utf8String Name
=> Utf8String.Empty;
public bool IsValid
=> false;
public bool Equals(IIdentifier? other)
=> false;
public override int GetHashCode()
=> 0;
public override string ToString()
=> "Invalid";
public IIdentifier CreatePermanent()
=> this;
public void ToJson(JsonTextWriter j)
{ }
}
public class PlayerIdentifier : IIdentifier, IEquatable<PlayerIdentifier>
{
public Utf8String Name { get; }
public readonly ushort HomeWorld;
public bool IsValid
=> true;
public PlayerIdentifier(Utf8String name, ushort homeWorld)
{
Name = name;
HomeWorld = homeWorld;
}
public bool Equals(IIdentifier? other)
=> Equals(other as PlayerIdentifier);
public bool Equals(PlayerIdentifier? other)
=> other?.HomeWorld == HomeWorld && other.Name.Equals(Name);
public override int GetHashCode()
=> HashCode.Combine(Name.Crc32, HomeWorld);
public override string ToString()
=> $"{Name} ({HomeWorld})";
public IIdentifier CreatePermanent()
=> new PlayerIdentifier(Name.Clone(), HomeWorld);
public void ToJson(JsonTextWriter j)
{
j.WriteStartObject();
j.WritePropertyName("Type");
j.WriteValue(GetType().Name);
j.WritePropertyName(nameof(Name));
j.WriteValue(Name);
j.WritePropertyName(nameof(HomeWorld));
j.WriteValue(HomeWorld);
j.WriteEndObject();
}
}
public class SpecialIdentifier : IIdentifier, IEquatable<SpecialIdentifier>
{
public Utf8String Name
=> Utf8String.Empty;
public readonly ushort Index;
public bool IsValid
=> true;
public SpecialIdentifier(ushort index)
=> Index = index;
public bool Equals(IIdentifier? other)
=> Equals(other as SpecialIdentifier);
public bool Equals(SpecialIdentifier? other)
=> other?.Index == Index;
public override int GetHashCode()
=> Index;
public override string ToString()
=> $"Special Actor {Index}";
public IIdentifier CreatePermanent()
=> this;
public void ToJson(JsonTextWriter j)
{
j.WriteStartObject();
j.WritePropertyName("Type");
j.WriteValue(GetType().Name);
j.WritePropertyName(nameof(Index));
j.WriteValue(Index);
j.WriteEndObject();
}
}
public class OwnedIdentifier : IIdentifier, IEquatable<OwnedIdentifier>
{
public Utf8String Name { get; }
public readonly Utf8String OwnerName;
public readonly uint DataId;
public readonly ushort OwnerHomeWorld;
public readonly ObjectKind Kind;
public bool IsValid
=> true;
public OwnedIdentifier(Utf8String name, Utf8String ownerName, ushort ownerHomeWorld, uint dataId, ObjectKind kind)
{
Name = name;
OwnerName = ownerName;
OwnerHomeWorld = ownerHomeWorld;
DataId = dataId;
Kind = kind;
}
public bool Equals(IIdentifier? other)
=> Equals(other as OwnedIdentifier);
public bool Equals(OwnedIdentifier? other)
=> other?.DataId == DataId
&& other.OwnerHomeWorld == OwnerHomeWorld
&& other.Kind == Kind
&& other.OwnerName.Equals(OwnerName);
public override int GetHashCode()
=> HashCode.Combine(OwnerName.Crc32, OwnerHomeWorld, DataId, Kind);
public override string ToString()
=> $"{OwnerName}s {Name}";
public IIdentifier CreatePermanent()
=> new OwnedIdentifier(Name.Clone(), OwnerName.Clone(), OwnerHomeWorld, DataId, Kind);
public void ToJson(JsonTextWriter j)
{
j.WriteStartObject();
j.WritePropertyName("Type");
j.WriteValue(GetType().Name);
j.WritePropertyName(nameof(Name));
j.WriteValue(Name);
j.WritePropertyName(nameof(OwnerName));
j.WriteValue(OwnerName);
j.WritePropertyName(nameof(OwnerHomeWorld));
j.WriteValue(OwnerHomeWorld);
j.WritePropertyName(nameof(Kind));
j.WriteValue(Kind);
j.WritePropertyName(nameof(DataId));
j.WriteValue(DataId);
j.WriteEndObject();
}
}
public class NpcIdentifier : IIdentifier, IEquatable<NpcIdentifier>
{
public Utf8String Name { get; }
public readonly uint DataId;
public readonly ushort ObjectIndex;
public bool IsValid
=> true;
public NpcIdentifier(Utf8String actorName, ushort objectIndex = ushort.MaxValue, uint dataId = uint.MaxValue)
{
Name = actorName;
ObjectIndex = objectIndex;
DataId = dataId;
}
public bool Equals(IIdentifier? other)
=> Equals(other as NpcIdentifier);
public bool Equals(NpcIdentifier? other)
=> (other?.Name.Equals(Name) ?? false)
&& (other.DataId == uint.MaxValue || DataId == uint.MaxValue || other.DataId == DataId)
&& (other.ObjectIndex == ushort.MaxValue || ObjectIndex == ushort.MaxValue || other.ObjectIndex == ObjectIndex);
public override int GetHashCode()
=> Name.Crc32;
public override string ToString()
=> DataId == uint.MaxValue ? ObjectIndex == ushort.MaxValue ? Name.ToString() : $"{Name} at {ObjectIndex}" :
ObjectIndex == ushort.MaxValue ? $"{Name} ({DataId})" : $"{Name} ({DataId}) at {ObjectIndex}";
public IIdentifier CreatePermanent()
=> new NpcIdentifier(Name.Clone(), ObjectIndex, DataId);
public void ToJson(JsonTextWriter j)
{
j.WriteStartObject();
j.WritePropertyName("Type");
j.WriteValue(GetType().Name);
j.WritePropertyName(nameof(Name));
j.WriteValue(Name);
j.WritePropertyName(nameof(DataId));
j.WriteValue(DataId);
j.WriteEndObject();
}
}
private static IIdentifier CreateIdentifier(Actor actor)
{
if (!actor.Valid)
return IIdentifier.Invalid;
var objectIdx = actor.Pointer->GameObject.ObjectIndex;
if (objectIdx is >= 200 and < 240)
{
var parentIdx = Glamourer.Penumbra.CutsceneParent(objectIdx);
if (parentIdx >= 0)
{
var parent = (Actor)Dalamud.Objects.GetObjectAddress(parentIdx);
if (!parent)
return IIdentifier.Invalid;
return CreateIdentifier(parent);
}
}
switch (actor.ObjectKind)
{
case ObjectKind.Player:
{
var name = actor.Utf8Name;
if (name.Length > 0 && actor.Pointer->HomeWorld is > 0 and < ushort.MaxValue)
return new PlayerIdentifier(actor.Utf8Name, actor.Pointer->HomeWorld);
return IIdentifier.Invalid;
}
case ObjectKind.BattleNpc:
{
var ownerId = actor.Pointer->GameObject.OwnerID;
if (ownerId != 0xE0000000)
{
var owner = (Actor)Dalamud.Objects.SearchById(ownerId)?.Address;
if (!owner)
return new InvalidIdentifier();
return new OwnedIdentifier(actor.Utf8Name, owner.Utf8Name, owner.Pointer->HomeWorld,
actor.Pointer->GameObject.DataID, ObjectKind.BattleNpc);
}
return new NpcIdentifier(actor.Utf8Name, actor.Pointer->GameObject.ObjectIndex,
actor.Pointer->GameObject.DataID);
}
case ObjectKind.Retainer:
case ObjectKind.EventNpc:
return new NpcIdentifier(actor.Utf8Name, actor.Pointer->GameObject.ObjectIndex,
actor.Pointer->GameObject.DataID);
case ObjectKind.MountType:
case ObjectKind.Companion:
{
var idx = actor.Pointer->GameObject.ObjectIndex;
if (idx % 2 == 0)
return new InvalidIdentifier();
var owner = (Actor)Dalamud.Objects[idx - 1]?.Address;
if (!owner)
return new InvalidIdentifier();
return new OwnedIdentifier(actor.Utf8Name, owner.Utf8Name, owner.Pointer->HomeWorld,
actor.Pointer->GameObject.DataID, actor.ObjectKind);
}
default: return new InvalidIdentifier();
}
}
}

232
Glamourer/Interop/Actor.cs Normal file
View file

@ -0,0 +1,232 @@
using System;
using Dalamud.Game.ClientState.Objects.Enums;
using Dalamud.Game.ClientState.Objects.Types;
using FFXIVClientStructs.FFXIV.Client.Graphics.Scene;
using Glamourer.Customization;
using Penumbra.GameData.ByteString;
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 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 uint Type
=> (*(delegate* unmanaged<Human*, uint>**)Pointer)[50](Pointer);
public Customize Customize
=> new((CustomizeData*)Pointer->CustomizeData);
public CharacterEquip Equip
=> new((CharacterArmor*)Pointer->EquipSlotData);
public unsafe CharacterWeapon MainHand
=> CharacterWeapon.Empty;
public unsafe CharacterWeapon OffHand
=> CharacterWeapon.Empty;
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;
}
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 IIdentifier GetIdentifier()
=> CreateIdentifier(this);
public bool Identifier(out IIdentifier ident)
{
if (Valid)
{
ident = GetIdentifier();
return true;
}
ident = IIdentifier.Invalid;
return false;
}
public Character? Character
=> Pointer == null ? null : Dalamud.Objects[Pointer->GameObject.ObjectIndex] as Character;
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 Utf8String Utf8Name
=> new(Pointer->GameObject.Name);
public byte Job
=> Pointer->ClassJob;
public DrawObject DrawObject
=> (IntPtr)Pointer->GameObject.DrawObject;
public bool Valid
=> Pointer != null;
public uint ModelId
{
get => (uint)Pointer->ModelCharaId;
set => Pointer->ModelCharaId = (int)value;
}
public Customize Customize
=> new((CustomizeData*)Pointer->CustomizeData);
public CharacterEquip Equip
=> new((CharacterArmor*)Pointer->EquipSlotData);
public unsafe CharacterWeapon MainHand
{
get => *(CharacterWeapon*)(Address + 0x06C0 + 0x10);
set => *(CharacterWeapon*)(Address + 0x06C0 + 0x10) = value;
}
public unsafe CharacterWeapon OffHand
{
get => *(CharacterWeapon*)(Address + 0x06C0 + 0x10 + 0x68);
set => *(CharacterWeapon*)(Address + 0x06C0 + 0x10 + 0x68) = 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 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

@ -0,0 +1,193 @@
using System.Collections.Generic;
using System.Text;
using Dalamud.Game.ClientState.Objects.Enums;
using Lumina.Excel.GeneratedSheets;
using static Glamourer.Interop.Actor;
namespace Glamourer.Interop;
public static class ObjectManager
{
private const int CutsceneIndex = 200;
private const int GPosePlayerIndex = 201;
private const int CharacterScreenIndex = 240;
private const int ExamineScreenIndex = 241;
private const int FittingRoomIndex = 242;
private const int DyePreviewIndex = 243;
private const int PortraitIndex = 244;
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 static bool IsInGPose { get; private set; }
public static ushort World { get; private set; }
public static IReadOnlyDictionary<IIdentifier, ActorData> Actors
=> Identifiers;
public static IReadOnlyList<(IIdentifier, ActorData)> List
=> ListData;
private static readonly Dictionary<IIdentifier, ActorData> Identifiers = new(200);
private static readonly List<(IIdentifier, ActorData)> ListData = new(Dalamud.Objects.Length);
private static void HandleIdentifier(IIdentifier identifier, Actor character)
{
if (!character.DrawObject)
return;
switch (identifier)
{
case PlayerIdentifier p:
if (!Identifiers.TryGetValue(p, out var data))
{
data = new ActorData(character,
World != p.HomeWorld
? $"{p.Name} ({Dalamud.GameData.GetExcelSheet<World>()!.GetRow(p.HomeWorld)!.Name})"
: p.Name.ToString());
Identifiers[p] = data;
ListData.Add((p, data));
}
else
{
data.Objects.Add(character);
}
break;
case NpcIdentifier n when !n.Name.IsEmpty:
if (!Identifiers.TryGetValue(n, out data))
{
data = new ActorData(character, $"{n.Name} (at {n.ObjectIndex})");
Identifiers[n] = data;
ListData.Add((n, data));
}
else
{
data.Objects.Add(character);
}
break;
case OwnedIdentifier o:
if (!Identifiers.TryGetValue(o, out data))
{
data = new ActorData(character,
World != o.OwnerHomeWorld
? $"{o.OwnerName}s {o.Name} ({Dalamud.GameData.GetExcelSheet<World>()!.GetRow(o.OwnerHomeWorld)!.Name})"
: $"{o.OwnerName}s {o.Name}");
Identifiers[o] = data;
ListData.Add((o, data));
}
else
{
data.Objects.Add(character);
}
break;
}
}
public static void Update()
{
World = (ushort)(Dalamud.ClientState.LocalPlayer?.CurrentWorld.Id ?? 0u);
Identifiers.Clear();
ListData.Clear();
for (var i = 0; i < CutsceneIndex; ++i)
{
Actor character = Dalamud.Objects.GetObjectAddress(i);
if (character.Identifier(out var identifier))
HandleIdentifier(identifier, character);
}
for (var i = CutsceneIndex; i < CharacterScreenIndex; ++i)
{
Actor character = Dalamud.Objects.GetObjectAddress(i);
if (!character.Identifier(out var identifier))
break;
HandleIdentifier(identifier, character);
}
void AddSpecial(int idx, string label)
{
Actor actor = Dalamud.Objects.GetObjectAddress(idx);
if (actor.Identifier(out var ident))
{
var data = new ActorData(actor, label);
Identifiers.Add(ident, data);
ListData.Add((ident, data));
}
}
AddSpecial(CharacterScreenIndex, "Character Screen Actor");
AddSpecial(ExamineScreenIndex, "Examine Screen Actor");
AddSpecial(FittingRoomIndex, "Fitting Room Actor");
AddSpecial(DyePreviewIndex, "Dye Preview Actor");
AddSpecial(PortraitIndex, "Portrait Actor");
for (var i = PortraitIndex + 1; i < Dalamud.Objects.Length; ++i)
{
Actor character = Dalamud.Objects.GetObjectAddress(i);
if (character.Identifier(out var identifier))
HandleIdentifier(identifier, character);
}
Actor gPose = Dalamud.Objects.GetObjectAddress(GPosePlayerIndex);
IsInGPose = gPose && gPose.Utf8Name.Length > 0;
}
public static Actor GPosePlayer
=> Dalamud.Objects.GetObjectAddress(GPosePlayerIndex);
public static Actor Player
=> Dalamud.Objects.GetObjectAddress(0);
private static unsafe string GetLabel(Actor player, string playerName, int num, bool gPose)
{
var sb = new StringBuilder(64);
sb.Append(playerName);
if (gPose)
{
sb.Append(" (GPose");
if (player.ObjectKind == ObjectKind.Player)
sb.Append(')');
else
sb.Append(player.ModelId == 0 ? ", NPC)" : ", Monster)");
}
else if (player.ObjectKind != ObjectKind.Player)
{
sb.Append(player.ModelId == 0 ? " (NPC)" : " (Monster)");
}
if (num > 1)
{
sb.Append(" #");
sb.Append(num);
}
return sb.ToString();
}
}

View file

@ -0,0 +1,23 @@
namespace Glamourer.Interop;
public static class Offsets
{
public static class Character
{
public const int Wetness = 0x1ADA;
public const int HatVisible = 0x84E;
public const int VisorToggled = 0x84F;
public const int WeaponHidden1 = 0x84F;
public const int WeaponHidden2 = 0x72C;
public const int Alpha = 0x19E0;
public static class Flags
{
public const byte IsHatHidden = 0x01;
public const byte IsVisorToggled = 0x08;
public const byte IsWet = 0x80;
public const byte IsWeaponHidden1 = 0x01;
public const byte IsWeaponHidden2 = 0x02;
}
}
}

View file

@ -0,0 +1,287 @@
using System;
using Dalamud.Hooking;
using Dalamud.Logging;
using Dalamud.Utility.Signatures;
using FFXIVClientStructs.FFXIV.Client.Graphics.Scene;
using Glamourer.Customization;
using Glamourer.State;
using Glamourer.Structs;
using Penumbra.GameData.Enums;
using Penumbra.GameData.Structs;
using Race = Penumbra.GameData.Enums.Race;
namespace Glamourer.Interop;
public unsafe partial class RedrawManager
{
private delegate void ChangeJobDelegate(IntPtr data, uint job);
[Signature("88 51 ?? 44 3B CA", DetourName = nameof(ChangeJobDetour))]
private readonly Hook<ChangeJobDelegate> _changeJobHook = null!;
private void ChangeJobDetour(IntPtr data, uint job)
{
_changeJobHook.Original(data, job);
JobChanged?.Invoke(data - 0x1A8, GameData.Jobs(Dalamud.GameData)[(byte)job]);
}
public event Action<Actor, Job>? JobChanged;
}
public unsafe partial class RedrawManager
{
public delegate ulong FlagSlotForUpdateDelegate(Human* drawObject, uint slot, CharacterArmor* data);
// This gets called when one of the ten equip items of an existing draw object gets changed.
[Signature("48 89 5C 24 ?? 48 89 74 24 ?? 57 48 83 EC 20 8B DA 49 8B F0 48 8B F9 83 FA 0A", DetourName = nameof(FlagSlotForUpdateDetour))]
private readonly Hook<FlagSlotForUpdateDelegate> _flagSlotForUpdateHook = null!;
private ulong FlagSlotForUpdateDetour(Human* drawObject, uint slotIdx, CharacterArmor* data)
{
var slot = slotIdx.ToEquipSlot();
try
{
var actor = Glamourer.Penumbra.GameObjectFromDrawObject((IntPtr)drawObject);
var identifier = actor.GetIdentifier();
if (_fixedDesigns.TryGetDesign(identifier, out var save))
{
PluginLog.Information($"Loaded {slot} from fixed design for {identifier}.");
(var replaced, *data) =
Glamourer.RestrictedGear.ResolveRestricted(save.Equipment[slot], slot, (Race)drawObject->Race, (Gender)drawObject->Sex);
}
else if (_currentManipulations.TryGetDesign(identifier, out var save2))
{
PluginLog.Information($"Updated {slot} from current designs for {identifier}.");
(var replaced, *data) =
Glamourer.RestrictedGear.ResolveRestricted(*data, slot, (Race)drawObject->Race, (Gender)drawObject->Sex);
save2.Data.Equipment[slot] = *data;
}
}
catch (Exception e)
{
PluginLog.Error($"Error on loading new gear:\n{e}");
}
return _flagSlotForUpdateHook.Original(drawObject, slotIdx, data);
}
public bool ChangeEquip(DrawObject drawObject, EquipSlot slot, CharacterArmor data)
{
if (!drawObject)
return false;
var slotIndex = slot.ToIndex();
if (slotIndex > 9)
return false;
return FlagSlotForUpdateDetour(drawObject.Pointer, slotIndex, &data) != 0;
}
public bool ChangeEquip(Actor actor, EquipSlot slot, CharacterArmor data)
=> actor && ChangeEquip(actor.DrawObject, slot, data);
}
public unsafe partial class RedrawManager
{
// The character weapon object manipulated is inside the actual character.
public const int CharacterWeaponOffset = 0x6C0;
public delegate void LoadWeaponDelegate(IntPtr offsetCharacter, uint slot, ulong weapon, byte redrawOnEquality, byte unk2,
byte skipGameObject,
byte unk4);
// 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("E8 ?? ?? ?? ?? 44 8B 9F", DetourName = nameof(LoadWeaponDetour))]
private readonly Hook<LoadWeaponDelegate> _loadWeaponHook = null!;
private void LoadWeaponDetour(IntPtr characterOffset, 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();
if (_fixedDesigns.TryGetDesign(identifier, out var save))
{
PluginLog.Information($"Loaded weapon from fixed design for {identifier}.");
weapon = slot switch
{
0 => save.MainHand.Value,
1 => save.OffHand.Value,
_ => weapon,
};
}
else if (redrawOnEquality == 1 && _currentManipulations.TryGetDesign(identifier, out var save2))
{
PluginLog.Information($"Loaded weapon from current design for {identifier}.");
switch (slot)
{
//case 0:
// save2.Data.MainHand = new CharacterWeapon(weapon);
// break;
//case 1:
// save.OffHand = new CharacterWeapon(weapon);
// break;
}
}
}
catch (Exception e)
{
PluginLog.Error($"Error on loading new weapon:\n{e}");
}
// First call the regular function.
_loadWeaponHook.Original(characterOffset, slot, oldWeapon, 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);
}
// Load a specific weapon for a character by its data and slot.
public void LoadWeapon(Actor character, EquipSlot slot, CharacterWeapon weapon)
{
switch (slot)
{
case EquipSlot.MainHand:
LoadWeaponDetour(character.Address + CharacterWeaponOffset, 0, weapon.Value, 0, 0, 1, 0);
return;
case EquipSlot.OffHand:
LoadWeaponDetour(character.Address + CharacterWeaponOffset, 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);
return;
// function can also be called with '2', but does not seem to ever be.
}
}
// Load specific Main- and Offhand weapons.
public void LoadWeapon(Actor character, CharacterWeapon main, CharacterWeapon off)
{
LoadWeaponDetour(character.Address + CharacterWeaponOffset, 0, main.Value, 0, 0, 1, 0);
LoadWeaponDetour(character.Address + CharacterWeaponOffset, 1, off.Value, 0, 0, 1, 0);
}
}
public unsafe partial class RedrawManager : IDisposable
{
private readonly FixedDesigns _fixedDesigns;
private readonly CurrentManipulations _currentManipulations;
public RedrawManager(FixedDesigns fixedDesigns, CurrentManipulations currentManipulations)
{
SignatureHelper.Initialise(this);
Glamourer.Penumbra.CreatingCharacterBase += OnCharacterRedraw;
Glamourer.Penumbra.CreatedCharacterBase += OnCharacterRedrawFinished;
_fixedDesigns = fixedDesigns;
_currentManipulations = currentManipulations;
_flagSlotForUpdateHook.Enable();
_loadWeaponHook.Enable();
_changeJobHook.Enable();
}
public void Dispose()
{
_flagSlotForUpdateHook.Dispose();
_loadWeaponHook.Dispose();
_changeJobHook.Dispose();
Glamourer.Penumbra.CreatingCharacterBase -= OnCharacterRedraw;
Glamourer.Penumbra.CreatedCharacterBase -= OnCharacterRedrawFinished;
}
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();
if (!(_currentManipulations.TryGetDesign(identifier, out var save) || _fixedDesigns.TryGetDesign(identifier, out var save2)))
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->CustomizeData);
//if (gameObjectCustomize.Equals(customize))
// customize.Load(save.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->EquipSlotData);
//if (gameObjectEquip.Equals(equip))
//{
// var saveEquip = save.Equipment;
// foreach (var slot in EquipSlotExtensions.EqdpSlots)
// {
// (var _, equip[slot]) =
// Glamourer.RestrictedGear.ResolveRestricted(true ? equip[slot] : saveEquip[slot], slot, customize.Race, customize.Gender);
// }
//}
}
private void OnCharacterRedraw(IntPtr gameObject, 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, IntPtr drawObject)
{
//SetVisor((Human*)drawObject, true);
if (Glamourer.Models.FromCharacterBase((CharacterBase*)drawObject, out var data))
PluginLog.Information($"Name: {data.FirstName} ({data.Id})");
else
PluginLog.Information($"Key: {Glamourer.Models.KeyFromCharacterBase((CharacterBase*)drawObject):X16}");
}
// Update
public delegate bool ChangeCustomizeDelegate(Human* human, byte* data, byte skipEquipment);
[Signature("E8 ?? ?? ?? ?? 41 0F B6 C5 66 41 89 86")]
private readonly ChangeCustomizeDelegate _changeCustomize = null!;
public bool UpdateCustomize(DrawObject drawObject, Customize customize)
{
if (!drawObject.Valid)
return false;
return _changeCustomize(drawObject.Pointer, (byte*)customize.Data, 1);
}
public static void SetVisor(Human* data, bool on)
{
if (data == null)
return;
var flags = &data->CharacterBase.UnkFlags_01;
var state = (*flags & 0x40) != 0;
if (state == on)
return;
*flags = (byte)((on ? *flags | 0x40 : *flags & 0xBF) | 0x80);
}
}