feat: Fate Table

This commit is contained in:
Raymond 2021-07-15 18:30:10 -04:00
parent a18bcc385c
commit 4e24c8fa3a
8 changed files with 465 additions and 1 deletions

View file

@ -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
/// </summary>
public ActorTable Actors { get; }
/// <summary>
/// Gets the table of all present fates.
/// </summary>
public FateTable Fates { get; }
/// <summary>
/// Gets the language of the client.
/// </summary>

View file

@ -1,4 +1,5 @@
using System;
using System.Runtime.InteropServices;
using Dalamud.Game.Internal;
@ -9,6 +10,8 @@ namespace Dalamud.Game.ClientState
/// </summary>
public sealed class ClientStateAddressResolver : BaseAddressResolver
{
private delegate IntPtr GetFateTableDelegate();
// Static offsets
/// <summary>
@ -16,6 +19,14 @@ namespace Dalamud.Game.ClientState
/// </summary>
public IntPtr ActorTable { get; private set; }
/// <summary>
/// Gets the address of the fate table pointer.
/// </summary>
/// <remarks>
/// This is a static address into the table, not the address of the table itself.
/// </remarks>
public IntPtr FateTablePtr { get; private set; }
// public IntPtr ViewportActorTable { get; private set; }
/// <summary>
@ -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<GetFateTableDelegate>(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;

View file

@ -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
{
/// <summary>
/// This collection represents the currently available Fate events.
/// </summary>
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;
/// <summary>
/// Initializes a new instance of the <see cref="FateTable"/> class.
/// </summary>
/// <param name="dalamud">The <see cref="dalamud"/> instance.</param>
/// <param name="addressResolver">Client state address resolver.</param>
internal FateTable(Dalamud dalamud, ClientStateAddressResolver addressResolver)
{
this.address = addressResolver;
this.dalamud = dalamud;
Log.Verbose($"Fate table address 0x{this.address.FateTablePtr.ToInt64():X}");
}
/// <summary>
/// Gets the amount of currently active Fates.
/// </summary>
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;
}
}
/// <summary>
/// Get an actor at the specified spawn index.
/// </summary>
/// <param name="index">Spawn index.</param>
/// <returns>A <see cref="Fate"/> at the specified spawn index.</returns>
[CanBeNull]
public Fate this[int index]
{
get
{
var address = this.GetFateAddress(index);
return this[address];
}
}
/// <summary>
/// Get a Fate at the specified address.
/// </summary>
/// <param name="address">The Fate address.</param>
/// <returns>A <see cref="Fate"/> at the specified address.</returns>
public Fate this[IntPtr address]
{
get
{
if (address == IntPtr.Zero)
return null;
return this.CreateFateReference(address);
}
}
/// <summary>
/// Gets the address of the Fate at the specified index of the fate table.
/// </summary>
/// <param name="index">The index of the Fate.</param>
/// <returns>The memory address of the Fate.</returns>
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));
}
/// <summary>
/// Create a reference to a FFXIV actor.
/// </summary>
/// <param name="offset">The offset of the actor in memory.</param>
/// <returns><see cref="Fate"/> object containing requested data.</returns>
[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);
}
}
/// <summary>
/// This collection represents the currently available Fate events.
/// </summary>
public sealed partial class FateTable : IReadOnlyCollection<Fate>, ICollection
{
/// <inheritdoc/>
int IReadOnlyCollection<Fate>.Count => this.Length;
/// <inheritdoc/>
int ICollection.Count => this.Length;
/// <inheritdoc/>
bool ICollection.IsSynchronized => false;
/// <inheritdoc/>
object ICollection.SyncRoot => this;
/// <inheritdoc/>
public IEnumerator<Fate> GetEnumerator()
{
for (var i = 0; i < this.Length; i++)
{
yield return this[i];
}
}
/// <inheritdoc/>
IEnumerator IEnumerable.GetEnumerator() => this.GetEnumerator();
/// <inheritdoc/>
void ICollection.CopyTo(Array array, int index)
{
for (var i = 0; i < this.Length; i++)
{
array.SetValue(this[i], index);
index++;
}
}
}
}

View file

@ -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
{
/// <summary>
/// This class represents an FFXIV Fate.
/// </summary>
public unsafe partial class Fate : IEquatable<Fate>
{
/// <summary>
/// Initializes a new instance of the <see cref="Fate"/> class.
/// </summary>
/// <param name="address">The address of this fate in memory.</param>
/// <param name="dalamud">Dalamud instance.</param>
internal Fate(IntPtr address, Dalamud dalamud)
{
this.Address = address;
this.Dalamud = dalamud;
}
/// <summary>
/// Gets the address of this Fate in memory.
/// </summary>
public IntPtr Address { get; }
/// <summary>
/// Gets Dalamud itself.
/// </summary>
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);
/// <summary>
/// Gets a value indicating whether this Fate is still valid in memory.
/// </summary>
/// <param name="fate">The fate to check.</param>
/// <returns>True or false.</returns>
public static bool IsValid(Fate fate)
{
if (fate == null)
return false;
if (fate.Dalamud.ClientState.LocalContentId == 0)
return false;
return true;
}
/// <summary>
/// Gets a value indicating whether this actor is still valid in memory.
/// </summary>
/// <returns>True or false.</returns>
public bool IsValid() => IsValid(this);
/// <inheritdoc/>
bool IEquatable<Fate>.Equals(Fate other) => this.FateId == other?.FateId;
/// <inheritdoc/>
public override bool Equals(object obj) => ((IEquatable<Fate>)this).Equals(obj as Fate);
/// <inheritdoc/>
public override int GetHashCode() => this.FateId.GetHashCode();
}
/// <summary>
/// This class represents an FFXIV Fate.
/// </summary>
public unsafe partial class Fate
{
/// <summary>
/// Gets the Fate ID of this <see cref="Fate" />.
/// </summary>
public ushort FateId => *(ushort*)(this.Address + FateOffsets.FateId);
/// <summary>
/// Gets game data linked to this Fate.
/// </summary>
public Lumina.Excel.GeneratedSheets.Fate GameData => this.Dalamud.Data.GetExcelSheet<Lumina.Excel.GeneratedSheets.Fate>().GetRow(this.FateId);
/// <summary>
/// Gets the time this <see cref="Fate"/> started.
/// </summary>
public int StartTimeEpoch => *(int*)(this.Address + FateOffsets.StartTimeEpoch);
/// <summary>
/// Gets how long this <see cref="Fate"/> will run.
/// </summary>
public short Duration => *(short*)(this.Address + FateOffsets.Duration);
/// <summary>
/// Gets the remaining time in seconds for this <see cref="Fate"/>.
/// </summary>
public long TimeRemaining => this.StartTimeEpoch + this.Duration - DateTimeOffset.Now.ToUnixTimeSeconds();
/// <summary>
/// Gets the displayname of this <see cref="Fate" />.
/// </summary>
public SeString Name => MemoryHelper.ReadSeString((Utf8String*)(this.Address + FateOffsets.Name));
/// <summary>
/// Gets the state of this <see cref="Fate"/> (Running, Ended, Failed, Preparation, WaitingForEnd).
/// </summary>
public FateState State => *(FateState*)(this.Address + FateOffsets.State);
/// <summary>
/// Gets the progress amount of this <see cref="Fate"/>.
/// </summary>
public byte Progress => *(byte*)(this.Address + FateOffsets.Progress);
/// <summary>
/// Gets the level of this <see cref="Fate"/>.
/// </summary>
public byte Level => *(byte*)(this.Address + FateOffsets.Level);
/// <summary>
/// Gets the position of this <see cref="Fate"/>.
/// </summary>
public Position3 Position => *(Position3*)(this.Address + FateOffsets.Position);
/// <summary>
/// Gets the territory this <see cref="Fate"/> is located in.
/// </summary>
public TerritoryTypeResolver TerritoryType => new(*(ushort*)(this.Address + FateOffsets.Territory), this.Dalamud);
}
}

View file

@ -0,0 +1,21 @@
using System.Diagnostics.CodeAnalysis;
namespace Dalamud.Game.ClientState.Fates.Types
{
/// <summary>
/// Memory offsets for the <see cref="Fate"/> type.
/// </summary>
[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;
}
}

View file

@ -0,0 +1,33 @@
namespace Dalamud.Game.ClientState.Fates.Types
{
/// <summary>
/// This represents the state of a single Fate.
/// </summary>
public enum FateState : byte
{
/// <summary>
/// The Fate is active.
/// </summary>
Running = 0x02,
/// <summary>
/// The Fate has ended.
/// </summary>
Ended = 0x04,
/// <summary>
/// The player failed the Fate.
/// </summary>
Failed = 0x05,
/// <summary>
/// The Fate is preparing to run.
/// </summary>
Preparation = 0x07,
/// <summary>
/// The Fate is preparing to end.
/// </summary>
WaitingForEnd = 0x08,
}
}

View file

@ -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;

View file

@ -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
}
}
/// <summary>
/// Read an SeString from a specified Utf8String structure.
/// </summary>
/// <param name="utf8String">The memory address to read from.</param>
/// <returns>The read in string.</returns>
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);
/// <summary>
/// Read an SeString from a specified Utf8String structure.
/// </summary>
/// <param name="utf8String">The memory address to read from.</param>
/// <param name="value">The read in string.</param>
public static unsafe void ReadSeString(Utf8String* utf8String, out SeString value)
=> value = ReadSeString(utf8String);
#endregion
#region Write