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