mirror of
https://github.com/Ottermandias/Glamourer.git
synced 2026-02-20 06:27:43 +01:00
blep
This commit is contained in:
parent
cb2e2f0128
commit
941bba1518
39 changed files with 2569 additions and 1579 deletions
350
Glamourer/Interop/Actor.Identifier.cs
Normal file
350
Glamourer/Interop/Actor.Identifier.cs
Normal 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
232
Glamourer/Interop/Actor.cs
Normal 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;
|
||||
}
|
||||
193
Glamourer/Interop/ObjectManager.cs
Normal file
193
Glamourer/Interop/ObjectManager.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
23
Glamourer/Interop/Offsets.cs
Normal file
23
Glamourer/Interop/Offsets.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
287
Glamourer/Interop/RedrawManager.cs
Normal file
287
Glamourer/Interop/RedrawManager.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue