From 4e24c8fa3ac7f6681886ab0698af9d4aeeed3009 Mon Sep 17 00:00:00 2001 From: Raymond Date: Thu, 15 Jul 2021 18:30:10 -0400 Subject: [PATCH] feat: Fate Table --- Dalamud/Game/ClientState/ClientState.cs | 8 + .../ClientState/ClientStateAddressResolver.cs | 16 ++ Dalamud/Game/ClientState/Fates/FateTable.cs | 178 ++++++++++++++++++ Dalamud/Game/ClientState/Fates/Types/Fate.cs | 138 ++++++++++++++ .../ClientState/Fates/Types/FateOffsets.cs | 21 +++ .../Game/ClientState/Fates/Types/FateState.cs | 33 ++++ .../Interface/Internal/Windows/DataWindow.cs | 43 +++++ Dalamud/Memory/MemoryHelper.cs | 29 ++- 8 files changed, 465 insertions(+), 1 deletion(-) create mode 100644 Dalamud/Game/ClientState/Fates/FateTable.cs create mode 100644 Dalamud/Game/ClientState/Fates/Types/Fate.cs create mode 100644 Dalamud/Game/ClientState/Fates/Types/FateOffsets.cs create mode 100644 Dalamud/Game/ClientState/Fates/Types/FateState.cs diff --git a/Dalamud/Game/ClientState/ClientState.cs b/Dalamud/Game/ClientState/ClientState.cs index d67bac813..b9e40ccd7 100644 --- a/Dalamud/Game/ClientState/ClientState.cs +++ b/Dalamud/Game/ClientState/ClientState.cs @@ -4,6 +4,7 @@ using System.Runtime.InteropServices; using Dalamud.Game.ClientState.Actors; using Dalamud.Game.ClientState.Actors.Types; +using Dalamud.Game.ClientState.Fates; using Dalamud.Game.Internal; using Dalamud.Hooking; using JetBrains.Annotations; @@ -42,6 +43,8 @@ namespace Dalamud.Game.ClientState this.Actors = new ActorTable(dalamud, this.address); + this.Fates = new FateTable(dalamud, this.address); + this.PartyList = new PartyList(dalamud, this.address); this.JobGauges = new JobGauges(this.address); @@ -97,6 +100,11 @@ namespace Dalamud.Game.ClientState /// public ActorTable Actors { get; } + /// + /// Gets the table of all present fates. + /// + public FateTable Fates { get; } + /// /// Gets the language of the client. /// diff --git a/Dalamud/Game/ClientState/ClientStateAddressResolver.cs b/Dalamud/Game/ClientState/ClientStateAddressResolver.cs index ed87f06d5..275809f82 100644 --- a/Dalamud/Game/ClientState/ClientStateAddressResolver.cs +++ b/Dalamud/Game/ClientState/ClientStateAddressResolver.cs @@ -1,4 +1,5 @@ using System; +using System.Runtime.InteropServices; using Dalamud.Game.Internal; @@ -9,6 +10,8 @@ namespace Dalamud.Game.ClientState /// public sealed class ClientStateAddressResolver : BaseAddressResolver { + private delegate IntPtr GetFateTableDelegate(); + // Static offsets /// @@ -16,6 +19,14 @@ namespace Dalamud.Game.ClientState /// public IntPtr ActorTable { get; private set; } + /// + /// Gets the address of the fate table pointer. + /// + /// + /// This is a static address into the table, not the address of the table itself. + /// + public IntPtr FateTablePtr { get; private set; } + // public IntPtr ViewportActorTable { get; private set; } /// @@ -68,8 +79,13 @@ namespace Dalamud.Game.ClientState // We don't need those anymore, but maybe someone else will - let's leave them here for good measure // ViewportActorTable = sig.GetStaticAddressFromSig("48 8D 0D ?? ?? ?? ?? 85 ED", 0) + 0x148; // SomeActorTableAccess = sig.ScanText("E8 ?? ?? ?? ?? 48 8D 55 A0 48 8D 8E ?? ?? ?? ??"); + this.ActorTable = sig.GetStaticAddressFromSig("48 8D 0D ?? ?? ?? ?? E8 ?? ?? ?? ?? 44 0F B6 83"); + var getFateTableAddr = sig.ScanText("E8 ?? ?? ?? ?? 48 8B C8 E8 ?? ?? ?? ?? 80 BF ?? ?? ?? ?? ??"); + var getFateTable = Marshal.GetDelegateForFunctionPointer(getFateTableAddr); + this.FateTablePtr = getFateTable(); + this.LocalContentId = sig.GetStaticAddressFromSig("48 0F 44 05 ?? ?? ?? ?? 48 39 07"); this.JobGaugeData = sig.GetStaticAddressFromSig("E8 ?? ?? ?? ?? FF C6 48 8D 5B 0C", 0xB9) + 0x10; diff --git a/Dalamud/Game/ClientState/Fates/FateTable.cs b/Dalamud/Game/ClientState/Fates/FateTable.cs new file mode 100644 index 000000000..07158820e --- /dev/null +++ b/Dalamud/Game/ClientState/Fates/FateTable.cs @@ -0,0 +1,178 @@ +using System; +using System.Collections; +using System.Collections.Generic; + +using Dalamud.Game.ClientState.Fates.Types; +using JetBrains.Annotations; +using Serilog; + +namespace Dalamud.Game.ClientState.Fates +{ + /// + /// This collection represents the currently available Fate events. + /// + public sealed partial class FateTable + { + // If the pointer at this offset is 0, do not scan the table + private const int CheckPtrOffset = 0x80; + private const int FirstPtrOffset = 0x90; + private const int LastPtrOffset = 0x98; + + private readonly Dalamud dalamud; + private readonly ClientStateAddressResolver address; + + /// + /// Initializes a new instance of the class. + /// + /// The instance. + /// Client state address resolver. + internal FateTable(Dalamud dalamud, ClientStateAddressResolver addressResolver) + { + this.address = addressResolver; + this.dalamud = dalamud; + + Log.Verbose($"Fate table address 0x{this.address.FateTablePtr.ToInt64():X}"); + } + + /// + /// Gets the amount of currently active Fates. + /// + public unsafe int Length + { + get + { + var fateTable = this.FateTableAddress; + if (fateTable == IntPtr.Zero) + return 0; + + var check = *(long*)(fateTable + CheckPtrOffset); + if (check == 0) + return 0; + + var start = *(long*)(fateTable + FirstPtrOffset); + var end = *(long*)(fateTable + LastPtrOffset); + if (start == 0 || end == 0) + return 0; + + return (int)((end - start) / 8); + } + } + + private unsafe IntPtr FateTableAddress + { + get + { + if (this.address.FateTablePtr == IntPtr.Zero) + return IntPtr.Zero; + + return *(IntPtr*)this.address.FateTablePtr; + } + } + + /// + /// Get an actor at the specified spawn index. + /// + /// Spawn index. + /// A at the specified spawn index. + [CanBeNull] + public Fate this[int index] + { + get + { + var address = this.GetFateAddress(index); + return this[address]; + } + } + + /// + /// Get a Fate at the specified address. + /// + /// The Fate address. + /// A at the specified address. + public Fate this[IntPtr address] + { + get + { + if (address == IntPtr.Zero) + return null; + + return this.CreateFateReference(address); + } + } + + /// + /// Gets the address of the Fate at the specified index of the fate table. + /// + /// The index of the Fate. + /// The memory address of the Fate. + public unsafe IntPtr GetFateAddress(int index) + { + if (index >= this.Length) + return IntPtr.Zero; + + var fateTable = this.FateTableAddress; + if (fateTable == IntPtr.Zero) + return IntPtr.Zero; + + var firstFate = *(IntPtr*)(fateTable + FirstPtrOffset); + return *(IntPtr*)(firstFate + (8 * index)); + } + + /// + /// Create a reference to a FFXIV actor. + /// + /// The offset of the actor in memory. + /// object containing requested data. + [CanBeNull] + internal unsafe Fate CreateFateReference(IntPtr offset) + { + if (this.dalamud.ClientState.LocalContentId == 0) + return null; + + if (offset == IntPtr.Zero) + return null; + + return new Fate(offset, this.dalamud); + } + } + + /// + /// This collection represents the currently available Fate events. + /// + public sealed partial class FateTable : IReadOnlyCollection, ICollection + { + /// + int IReadOnlyCollection.Count => this.Length; + + /// + int ICollection.Count => this.Length; + + /// + bool ICollection.IsSynchronized => false; + + /// + object ICollection.SyncRoot => this; + + /// + public IEnumerator GetEnumerator() + { + for (var i = 0; i < this.Length; i++) + { + yield return this[i]; + } + } + + /// + IEnumerator IEnumerable.GetEnumerator() => this.GetEnumerator(); + + /// + void ICollection.CopyTo(Array array, int index) + { + for (var i = 0; i < this.Length; i++) + { + array.SetValue(this[i], index); + index++; + } + } + } +} diff --git a/Dalamud/Game/ClientState/Fates/Types/Fate.cs b/Dalamud/Game/ClientState/Fates/Types/Fate.cs new file mode 100644 index 000000000..a42e3f925 --- /dev/null +++ b/Dalamud/Game/ClientState/Fates/Types/Fate.cs @@ -0,0 +1,138 @@ +using System; + +using Dalamud.Game.ClientState.Resolvers; +using Dalamud.Game.Text.SeStringHandling; +using Dalamud.Memory; +using FFXIVClientStructs.FFXIV.Client.System.String; + +namespace Dalamud.Game.ClientState.Fates.Types +{ + /// + /// This class represents an FFXIV Fate. + /// + public unsafe partial class Fate : IEquatable + { + /// + /// Initializes a new instance of the class. + /// + /// The address of this fate in memory. + /// Dalamud instance. + internal Fate(IntPtr address, Dalamud dalamud) + { + this.Address = address; + this.Dalamud = dalamud; + } + + /// + /// Gets the address of this Fate in memory. + /// + public IntPtr Address { get; } + + /// + /// Gets Dalamud itself. + /// + private protected Dalamud Dalamud { get; } + + public static bool operator ==(Fate fate1, Fate fate2) + { + if (fate1 is null || fate2 is null) + return Equals(fate1, fate2); + + return fate1.Equals(fate2); + } + + public static bool operator !=(Fate fate1, Fate fate2) => !(fate1 == fate2); + + /// + /// Gets a value indicating whether this Fate is still valid in memory. + /// + /// The fate to check. + /// True or false. + public static bool IsValid(Fate fate) + { + if (fate == null) + return false; + + if (fate.Dalamud.ClientState.LocalContentId == 0) + return false; + + return true; + } + + /// + /// Gets a value indicating whether this actor is still valid in memory. + /// + /// True or false. + public bool IsValid() => IsValid(this); + + /// + bool IEquatable.Equals(Fate other) => this.FateId == other?.FateId; + + /// + public override bool Equals(object obj) => ((IEquatable)this).Equals(obj as Fate); + + /// + public override int GetHashCode() => this.FateId.GetHashCode(); + } + + /// + /// This class represents an FFXIV Fate. + /// + public unsafe partial class Fate + { + /// + /// Gets the Fate ID of this . + /// + public ushort FateId => *(ushort*)(this.Address + FateOffsets.FateId); + + /// + /// Gets game data linked to this Fate. + /// + public Lumina.Excel.GeneratedSheets.Fate GameData => this.Dalamud.Data.GetExcelSheet().GetRow(this.FateId); + + /// + /// Gets the time this started. + /// + public int StartTimeEpoch => *(int*)(this.Address + FateOffsets.StartTimeEpoch); + + /// + /// Gets how long this will run. + /// + public short Duration => *(short*)(this.Address + FateOffsets.Duration); + + /// + /// Gets the remaining time in seconds for this . + /// + public long TimeRemaining => this.StartTimeEpoch + this.Duration - DateTimeOffset.Now.ToUnixTimeSeconds(); + + /// + /// Gets the displayname of this . + /// + public SeString Name => MemoryHelper.ReadSeString((Utf8String*)(this.Address + FateOffsets.Name)); + + /// + /// Gets the state of this (Running, Ended, Failed, Preparation, WaitingForEnd). + /// + public FateState State => *(FateState*)(this.Address + FateOffsets.State); + + /// + /// Gets the progress amount of this . + /// + public byte Progress => *(byte*)(this.Address + FateOffsets.Progress); + + /// + /// Gets the level of this . + /// + public byte Level => *(byte*)(this.Address + FateOffsets.Level); + + /// + /// Gets the position of this . + /// + public Position3 Position => *(Position3*)(this.Address + FateOffsets.Position); + + /// + /// Gets the territory this is located in. + /// + public TerritoryTypeResolver TerritoryType => new(*(ushort*)(this.Address + FateOffsets.Territory), this.Dalamud); + } +} diff --git a/Dalamud/Game/ClientState/Fates/Types/FateOffsets.cs b/Dalamud/Game/ClientState/Fates/Types/FateOffsets.cs new file mode 100644 index 000000000..73bc7a702 --- /dev/null +++ b/Dalamud/Game/ClientState/Fates/Types/FateOffsets.cs @@ -0,0 +1,21 @@ +using System.Diagnostics.CodeAnalysis; + +namespace Dalamud.Game.ClientState.Fates.Types +{ + /// + /// Memory offsets for the type. + /// + [SuppressMessage("StyleCop.CSharp.DocumentationRules", "SA1600:Elements should be documented", Justification = "Document the offset usage instead.")] + public static class FateOffsets + { + public const int FateId = 0x18; + public const int StartTimeEpoch = 0x20; + public const int Duration = 0x28; + public const int Name = 0xC0; + public const int State = 0x3AC; + public const int Progress = 0x3B8; + public const int Level = 0x3F9; + public const int Position = 0x450; + public const int Territory = 0x74E; + } +} diff --git a/Dalamud/Game/ClientState/Fates/Types/FateState.cs b/Dalamud/Game/ClientState/Fates/Types/FateState.cs new file mode 100644 index 000000000..94eb00717 --- /dev/null +++ b/Dalamud/Game/ClientState/Fates/Types/FateState.cs @@ -0,0 +1,33 @@ +namespace Dalamud.Game.ClientState.Fates.Types +{ + /// + /// This represents the state of a single Fate. + /// + public enum FateState : byte + { + /// + /// The Fate is active. + /// + Running = 0x02, + + /// + /// The Fate has ended. + /// + Ended = 0x04, + + /// + /// The player failed the Fate. + /// + Failed = 0x05, + + /// + /// The Fate is preparing to run. + /// + Preparation = 0x07, + + /// + /// The Fate is preparing to end. + /// + WaitingForEnd = 0x08, + } +} diff --git a/Dalamud/Interface/Internal/Windows/DataWindow.cs b/Dalamud/Interface/Internal/Windows/DataWindow.cs index f107a8041..6b723098f 100644 --- a/Dalamud/Interface/Internal/Windows/DataWindow.cs +++ b/Dalamud/Interface/Internal/Windows/DataWindow.cs @@ -86,6 +86,7 @@ namespace Dalamud.Interface.Internal.Windows Server_OpCode, Address, Actor_Table, + Fate_Table, Font_Test, Party_List, Plugin_IPC, @@ -177,6 +178,8 @@ namespace Dalamud.Interface.Internal.Windows this.DrawActorTable(); break; + case DataKind.Fate_Table: + this.DrawFateTable(); break; case DataKind.Font_Test: @@ -373,6 +376,46 @@ namespace Dalamud.Interface.Internal.Windows } } + private void DrawFateTable() + { + var stateString = string.Empty; + if (this.dalamud.ClientState.Fates.Length == 0) + { + ImGui.TextUnformatted("No fates or data not ready."); + } + else + { + stateString += $"FrameworkBase: {this.dalamud.Framework.Address.BaseAddress.ToInt64():X}\n"; + stateString += $"FateTableLen: {this.dalamud.ClientState.Fates.Length}\n"; + + ImGui.TextUnformatted(stateString); + + for (var i = 0; i < this.dalamud.ClientState.Fates.Length; i++) + { + var fate = this.dalamud.ClientState.Fates[i]; + if (fate == null) + continue; + + var fateString = $"{fate.Address.ToInt64():X}:[{i}]" + + $" - Lv.{fate.Level} {fate.Name} ({fate.Progress}%)" + + $" - X{fate.Position.X} Y{fate.Position.Y} Z{fate.Position.Z}" + + $" - Territory {(this.resolveGameData ? (fate.TerritoryType.GameData?.Name ?? fate.TerritoryType.Id.ToString()) : fate.TerritoryType.Id.ToString())}\n"; + + fateString += $" StartTimeEpoch: {fate.StartTimeEpoch}" + + $" - Duration: {fate.Duration}" + + $" - State: {fate.State}" + + $" - GameData name: {(this.resolveGameData ? (fate.GameData?.Name ?? fate.FateId.ToString()) : fate.FateId.ToString())}"; + + ImGui.TextUnformatted(fateString); + ImGui.SameLine(); + if (ImGui.Button("C")) + { + ImGui.SetClipboardText(fate.Address.ToString("X")); + } + } + } + } + private void DrawFontTest() { var specialChars = string.Empty; diff --git a/Dalamud/Memory/MemoryHelper.cs b/Dalamud/Memory/MemoryHelper.cs index b962d637e..66a5908ee 100644 --- a/Dalamud/Memory/MemoryHelper.cs +++ b/Dalamud/Memory/MemoryHelper.cs @@ -6,7 +6,7 @@ using System.Text; using Dalamud.Game.Text.SeStringHandling; using Dalamud.Memory.Exceptions; - +using FFXIVClientStructs.FFXIV.Client.System.String; using static Dalamud.NativeFunctions; // Heavily inspired from Reloaded (https://github.com/Reloaded-Project/Reloaded.Memory) @@ -237,6 +237,25 @@ namespace Dalamud.Memory } } + /// + /// Read an SeString from a specified Utf8String structure. + /// + /// The memory address to read from. + /// The read in string. + public static unsafe SeString ReadSeString(Utf8String* utf8String) + { + if (utf8String == null) + return string.Empty; + + var ptr = utf8String->StringPtr; + if (ptr == null) + return string.Empty; + + var len = Math.Max(utf8String->BufUsed, utf8String->StringLength); + + return ReadSeString((IntPtr)ptr, (int)len); + } + #endregion #region ReadString(out) @@ -306,6 +325,14 @@ namespace Dalamud.Memory public static void ReadSeString(IntPtr memoryAddress, int maxLength, out SeString value) => value = ReadSeString(memoryAddress, maxLength); + /// + /// Read an SeString from a specified Utf8String structure. + /// + /// The memory address to read from. + /// The read in string. + public static unsafe void ReadSeString(Utf8String* utf8String, out SeString value) + => value = ReadSeString(utf8String); + #endregion #region Write