[api11] Some code cleanup and signature replacements (#2066)

* Remove unused code from ChatHandlers

* Replace sigs in DalamudAtkTweaks

* Resolve LocalContentId by using PlayerState.ContentId

* Resolve BuddyList address via UIState.Buddy

* Resolve ObjectTable address via GameObjectManager

* Resolve FateTable address via FateManager

* Resolve GroupManager address via GroupManager

* Resolve JobGauges address via JobGaugeManager.CurrentGauge

* Simplify ItemHover/Out event

* Resolve ToggleUiHide address via RaptureAtkModule.SetUiVisibility

* Resolve PopulateItemLinkObject via InventoryItem.Copy

* Add byte[].AsPointer extension

* Resolve addresses used by ToastGui via UIModule functions

* Use Length from Span as ObjectTableLength

* Replace OpenMapWithMapLink with CS call

* Resolve FrameworkAddressResolver with CS vtable

* Drop unnecessary ToArray in HandlePrintMessage

* Clean up event calls in HandlePrintMessageDetour

* Simplify LocalContentId further

This pointer can't be null, because it's part of the .data section.

* Compare SeStrings in FlyTextGui with SequenceEqual

* Use CS types in FlyTextGuis internal code

* Simplify reading SeStrings internally

* Remove AsPointer again

* Resolve Number/StringArray by type in NamePlateGui

* Fix crashes in HandlePrintMessageDetour

* Resolve InteractableLinkClicked with LogViewer.HandleLinkClick
This commit is contained in:
Haselnussbomber 2024-11-12 17:20:29 +01:00 committed by GitHub
parent 084f8b55e7
commit c0f05614c6
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
25 changed files with 343 additions and 827 deletions

View file

@ -1,22 +1,14 @@
using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Reflection; using System.Reflection;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
using CheapLoc; using CheapLoc;
using Dalamud.Configuration.Internal; using Dalamud.Configuration.Internal;
using Dalamud.Game.ClientState.Conditions;
using Dalamud.Game.Gui; using Dalamud.Game.Gui;
using Dalamud.Game.Text; using Dalamud.Game.Text;
using Dalamud.Game.Text.SeStringHandling; using Dalamud.Game.Text.SeStringHandling;
using Dalamud.Game.Text.SeStringHandling.Payloads;
using Dalamud.Interface;
using Dalamud.Interface.ImGuiNotification;
using Dalamud.Interface.ImGuiNotification.Internal;
using Dalamud.Interface.Internal; using Dalamud.Interface.Internal;
using Dalamud.Interface.Internal.Windows;
using Dalamud.Logging.Internal; using Dalamud.Logging.Internal;
using Dalamud.Plugin.Internal; using Dalamud.Plugin.Internal;
using Dalamud.Utility; using Dalamud.Utility;
@ -27,49 +19,10 @@ namespace Dalamud.Game;
/// Chat events and public helper functions. /// Chat events and public helper functions.
/// </summary> /// </summary>
[ServiceManager.EarlyLoadedService] [ServiceManager.EarlyLoadedService]
internal class ChatHandlers : IServiceType internal partial class ChatHandlers : IServiceType
{ {
private static readonly ModuleLog Log = new("CHATHANDLER"); private static readonly ModuleLog Log = new("ChatHandlers");
private readonly Dictionary<ClientLanguage, Regex[]> retainerSaleRegexes = new()
{
{
ClientLanguage.Japanese,
new Regex[]
{
new Regex(@"^(?:.+)マーケットに(?<origValue>[\d,.]+)ギルで出品した(?<item>.*)×(?<count>[\d,.]+)が売れ、(?<value>[\d,.]+)ギルを入手しました。$", RegexOptions.Compiled),
new Regex(@"^(?:.+)マーケットに(?<origValue>[\d,.]+)ギルで出品した(?<item>.*)が売れ、(?<value>[\d,.]+)ギルを入手しました。$", RegexOptions.Compiled),
}
},
{
ClientLanguage.English,
new Regex[]
{
new Regex(@"^(?<item>.+) you put up for sale in the (?:.+) markets (?:have|has) sold for (?<value>[\d,.]+) gil \(after fees\)\.$", RegexOptions.Compiled),
}
},
{
ClientLanguage.German,
new Regex[]
{
new Regex(@"^Dein Gehilfe hat (?<item>.+) auf dem Markt von (?:.+) für (?<value>[\d,.]+) Gil verkauft\.$", RegexOptions.Compiled),
new Regex(@"^Dein Gehilfe hat (?<item>.+) auf dem Markt von (?:.+) verkauft und (?<value>[\d,.]+) Gil erhalten\.$", RegexOptions.Compiled),
}
},
{
ClientLanguage.French,
new Regex[]
{
new Regex(@"^Un servant a vendu (?<item>.+) pour (?<value>[\d,.]+) gil à (?:.+)\.$", RegexOptions.Compiled),
}
},
};
private readonly Regex urlRegex = new(@"(http|ftp|https)://([\w_-]+(?:(?:\.[\w_-]+)+))([\w.,@?^=%&:/~+#-]*[\w@?^=%&/~+#-])?", RegexOptions.Compiled);
[ServiceManager.ServiceDependency]
private readonly Dalamud dalamud = Service<Dalamud>.Get();
[ServiceManager.ServiceDependency] [ServiceManager.ServiceDependency]
private readonly DalamudConfiguration configuration = Service<DalamudConfiguration>.Get(); private readonly DalamudConfiguration configuration = Service<DalamudConfiguration>.Get();
@ -92,6 +45,9 @@ internal class ChatHandlers : IServiceType
/// </summary> /// </summary>
public bool IsAutoUpdateComplete { get; private set; } public bool IsAutoUpdateComplete { get; private set; }
[GeneratedRegex(@"(http|ftp|https)://([\w_-]+(?:(?:\.[\w_-]+)+))([\w.,@?^=%&:/~+#-]*[\w@?^=%&/~+#-])?", RegexOptions.Compiled)]
private static partial Regex CompiledUrlRegex();
private void OnCheckMessageHandled(XivChatType type, int timestamp, ref SeString sender, ref SeString message, ref bool isHandled) private void OnCheckMessageHandled(XivChatType type, int timestamp, ref SeString sender, ref SeString message, ref bool isHandled)
{ {
var textVal = message.TextValue; var textVal = message.TextValue;
@ -100,7 +56,7 @@ internal class ChatHandlers : IServiceType
this.configuration.BadWords.Any(x => !string.IsNullOrEmpty(x) && textVal.Contains(x))) this.configuration.BadWords.Any(x => !string.IsNullOrEmpty(x) && textVal.Contains(x)))
{ {
// This seems to be in the user block list - let's not show it // This seems to be in the user block list - let's not show it
Log.Debug("Blocklist triggered"); Log.Debug("Filtered a message that contained a muted word");
isHandled = true; isHandled = true;
return; return;
} }
@ -127,41 +83,10 @@ internal class ChatHandlers : IServiceType
return; return;
#endif #endif
if (type == XivChatType.RetainerSale)
{
foreach (var regex in this.retainerSaleRegexes[(ClientLanguage)this.dalamud.StartInfo.Language])
{
var matchInfo = regex.Match(message.TextValue);
// we no longer really need to do/validate the item matching since we read the id from the byte array
// but we'd be checking the main match anyway
var itemInfo = matchInfo.Groups["item"];
if (!itemInfo.Success)
continue;
var itemLink = message.Payloads.FirstOrDefault(x => x.Type == PayloadType.Item) as ItemPayload;
if (itemLink == default)
{
Log.Error("itemLink was null. Msg: {0}", BitConverter.ToString(message.Encode()));
break;
}
Log.Debug($"Probable retainer sale: {message}, decoded item {itemLink.Item.RowId}, HQ {itemLink.IsHQ}");
var valueInfo = matchInfo.Groups["value"];
// not sure if using a culture here would work correctly, so just strip symbols instead
if (!valueInfo.Success || !int.TryParse(valueInfo.Value.Replace(",", string.Empty).Replace(".", string.Empty), out var itemValue))
continue;
// Task.Run(() => this.dalamud.BotManager.ProcessRetainerSale(itemLink.Item.RowId, itemValue, itemLink.IsHQ));
break;
}
}
var messageCopy = message; var messageCopy = message;
var senderCopy = sender; var senderCopy = sender;
var linkMatch = this.urlRegex.Match(message.TextValue); var linkMatch = CompiledUrlRegex().Match(message.TextValue);
if (linkMatch.Value.Length > 0) if (linkMatch.Value.Length > 0)
this.LastLink = linkMatch.Value; this.LastLink = linkMatch.Value;
} }

View file

@ -1,14 +1,12 @@
using System.Collections; using System.Collections;
using System.Collections.Generic; using System.Collections.Generic;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using Dalamud.IoC; using Dalamud.IoC;
using Dalamud.IoC.Internal; using Dalamud.IoC.Internal;
using Dalamud.Plugin.Services; using Dalamud.Plugin.Services;
using Dalamud.Utility;
using Serilog; using FFXIVClientStructs.FFXIV.Client.Game.UI;
namespace Dalamud.Game.ClientState.Buddy; namespace Dalamud.Game.ClientState.Buddy;
@ -28,14 +26,9 @@ internal sealed partial class BuddyList : IServiceType, IBuddyList
[ServiceManager.ServiceDependency] [ServiceManager.ServiceDependency]
private readonly ClientState clientState = Service<ClientState>.Get(); private readonly ClientState clientState = Service<ClientState>.Get();
private readonly ClientStateAddressResolver address;
[ServiceManager.ServiceConstructor] [ServiceManager.ServiceConstructor]
private BuddyList() private BuddyList()
{ {
this.address = this.clientState.AddressResolver;
Log.Verbose($"Buddy list address {Util.DescribeAddress(this.address.BuddyList)}");
} }
/// <inheritdoc/> /// <inheritdoc/>
@ -76,14 +69,7 @@ internal sealed partial class BuddyList : IServiceType, IBuddyList
} }
} }
/// <summary> private unsafe FFXIVClientStructs.FFXIV.Client.Game.UI.Buddy* BuddyListStruct => &UIState.Instance()->Buddy;
/// Gets the address of the buddy list.
/// </summary>
internal IntPtr BuddyListAddress => this.address.BuddyList;
private static int BuddyMemberSize { get; } = Marshal.SizeOf<FFXIVClientStructs.FFXIV.Client.Game.UI.Buddy.BuddyMember>();
private unsafe FFXIVClientStructs.FFXIV.Client.Game.UI.Buddy* BuddyListStruct => (FFXIVClientStructs.FFXIV.Client.Game.UI.Buddy*)this.BuddyListAddress;
/// <inheritdoc/> /// <inheritdoc/>
public IBuddyMember? this[int index] public IBuddyMember? this[int index]

View file

@ -17,6 +17,7 @@ using Dalamud.Utility;
using FFXIVClientStructs.FFXIV.Application.Network; using FFXIVClientStructs.FFXIV.Application.Network;
using FFXIVClientStructs.FFXIV.Client.Game; using FFXIVClientStructs.FFXIV.Client.Game;
using FFXIVClientStructs.FFXIV.Client.Game.Event; using FFXIVClientStructs.FFXIV.Client.Game.Event;
using FFXIVClientStructs.FFXIV.Client.Game.UI;
using FFXIVClientStructs.FFXIV.Client.UI; using FFXIVClientStructs.FFXIV.Client.UI;
using FFXIVClientStructs.FFXIV.Client.UI.Agent; using FFXIVClientStructs.FFXIV.Client.UI.Agent;
@ -111,7 +112,7 @@ internal sealed class ClientState : IInternalDisposableService, IClientState
get get
{ {
var agentMap = AgentMap.Instance(); var agentMap = AgentMap.Instance();
return agentMap != null ? AgentMap.Instance()->CurrentMapId : 0; return agentMap != null ? agentMap->CurrentMapId : 0;
} }
} }
@ -119,7 +120,7 @@ internal sealed class ClientState : IInternalDisposableService, IClientState
public IPlayerCharacter? LocalPlayer => Service<ObjectTable>.GetNullable()?[0] as IPlayerCharacter; public IPlayerCharacter? LocalPlayer => Service<ObjectTable>.GetNullable()?[0] as IPlayerCharacter;
/// <inheritdoc/> /// <inheritdoc/>
public ulong LocalContentId => (ulong)Marshal.ReadInt64(this.address.LocalContentId); public unsafe ulong LocalContentId => PlayerState.Instance()->ContentId;
/// <inheritdoc/> /// <inheritdoc/>
public bool IsLoggedIn { get; private set; } public bool IsLoggedIn { get; private set; }

View file

@ -7,39 +7,6 @@ internal sealed class ClientStateAddressResolver : BaseAddressResolver
{ {
// Static offsets // Static offsets
/// <summary>
/// Gets the address of the actor table.
/// </summary>
public IntPtr ObjectTable { get; private set; }
/// <summary>
/// Gets the address of the buddy list.
/// </summary>
public IntPtr BuddyList { get; private set; }
/// <summary>
/// Gets the address of a pointer to the fate table.
/// </summary>
/// <remarks>
/// This is a static address to a pointer, not the address of the table itself.
/// </remarks>
public IntPtr FateTablePtr { get; private set; }
/// <summary>
/// Gets the address of the Group Manager.
/// </summary>
public IntPtr GroupManager { get; private set; }
/// <summary>
/// Gets the address of the local content id.
/// </summary>
public IntPtr LocalContentId { get; private set; }
/// <summary>
/// Gets the address of job gauge data.
/// </summary>
public IntPtr JobGaugeData { get; private set; }
/// <summary> /// <summary>
/// Gets the address of the keyboard state. /// Gets the address of the keyboard state.
/// </summary> /// </summary>
@ -74,17 +41,6 @@ internal sealed class ClientStateAddressResolver : BaseAddressResolver
/// <param name="sig">The signature scanner to facilitate setup.</param> /// <param name="sig">The signature scanner to facilitate setup.</param>
protected override void Setup64Bit(ISigScanner sig) protected override void Setup64Bit(ISigScanner sig)
{ {
this.ObjectTable = sig.GetStaticAddressFromSig("48 8D 0D ?? ?? ?? ?? E8 ?? ?? ?? ?? 44 0F B6 83 ?? ?? ?? ?? C6 83 ?? ?? ?? ?? ??");
this.BuddyList = sig.GetStaticAddressFromSig("48 8D 0D ?? ?? ?? ?? E8 ?? ?? ?? ?? 45 84 E4 75 1A F6 45 12 04");
this.FateTablePtr = sig.GetStaticAddressFromSig("48 8B 15 ?? ?? ?? ?? 48 8B F1 44 0F B7 41");
this.GroupManager = sig.GetStaticAddressFromSig("48 8D 0D ?? ?? ?? ?? E8 ?? ?? ?? ?? 80 B8 ?? ?? ?? ?? ?? 77 71");
this.LocalContentId = sig.GetStaticAddressFromSig("48 0F 44 0D ?? ?? ?? ?? 48 8D 57 08");
this.JobGaugeData = sig.GetStaticAddressFromSig("48 8B 3D ?? ?? ?? ?? 33 ED") + 0x8;
this.SetupTerritoryType = sig.ScanText("48 89 5C 24 ?? 48 89 6C 24 ?? 57 48 83 EC 20 0F B7 DA"); this.SetupTerritoryType = sig.ScanText("48 89 5C 24 ?? 48 89 6C 24 ?? 57 48 83 EC 20 0F B7 DA");
this.ProcessPacketPlayerSetup = sig.ScanText("40 53 48 83 EC 20 48 8D 0D ?? ?? ?? ?? 48 8B DA E8 ?? ?? ?? ?? 48 8B D3"); this.ProcessPacketPlayerSetup = sig.ScanText("40 53 48 83 EC 20 48 8D 0D ?? ?? ?? ?? 48 8B DA E8 ?? ?? ?? ?? 48 8B D3");

View file

@ -4,9 +4,8 @@ using System.Collections.Generic;
using Dalamud.IoC; using Dalamud.IoC;
using Dalamud.IoC.Internal; using Dalamud.IoC.Internal;
using Dalamud.Plugin.Services; using Dalamud.Plugin.Services;
using Dalamud.Utility;
using Serilog; using CSFateManager = FFXIVClientStructs.FFXIV.Client.Game.Fate.FateManager;
namespace Dalamud.Game.ClientState.Fates; namespace Dalamud.Game.ClientState.Fates;
@ -20,55 +19,34 @@ namespace Dalamud.Game.ClientState.Fates;
#pragma warning restore SA1015 #pragma warning restore SA1015
internal sealed partial class FateTable : IServiceType, IFateTable internal sealed partial class FateTable : IServiceType, IFateTable
{ {
private readonly ClientStateAddressResolver address;
[ServiceManager.ServiceConstructor] [ServiceManager.ServiceConstructor]
private FateTable(ClientState clientState) private FateTable()
{ {
this.address = clientState.AddressResolver;
Log.Verbose($"Fate table address {Util.DescribeAddress(this.address.FateTablePtr)}");
} }
/// <inheritdoc/> /// <inheritdoc/>
public IntPtr Address => this.address.FateTablePtr; public unsafe IntPtr Address => (nint)CSFateManager.Instance();
/// <inheritdoc/> /// <inheritdoc/>
public unsafe int Length public unsafe int Length
{ {
get get
{ {
var fateTable = this.FateTableAddress; var fateManager = CSFateManager.Instance();
if (fateTable == IntPtr.Zero) if (fateManager == null)
return 0; return 0;
// Sonar used this to check if the table was safe to read // Sonar used this to check if the table was safe to read
if (Struct->FateDirector == null) if (fateManager->FateDirector == null)
return 0; return 0;
if (Struct->Fates.First == null || Struct->Fates.Last == null) if (fateManager->Fates.First == null || fateManager->Fates.Last == null)
return 0; return 0;
return Struct->Fates.Count; return fateManager->Fates.Count;
} }
} }
/// <summary>
/// Gets the address of the Fate table.
/// </summary>
internal unsafe IntPtr FateTableAddress
{
get
{
if (this.address.FateTablePtr == IntPtr.Zero)
return IntPtr.Zero;
return *(IntPtr*)this.address.FateTablePtr;
}
}
private unsafe FFXIVClientStructs.FFXIV.Client.Game.Fate.FateManager* Struct => (FFXIVClientStructs.FFXIV.Client.Game.Fate.FateManager*)this.FateTableAddress;
/// <inheritdoc/> /// <inheritdoc/>
public IFate? this[int index] public IFate? this[int index]
{ {
@ -99,11 +77,11 @@ internal sealed partial class FateTable : IServiceType, IFateTable
if (index >= this.Length) if (index >= this.Length)
return IntPtr.Zero; return IntPtr.Zero;
var fateTable = this.FateTableAddress; var fateManager = CSFateManager.Instance();
if (fateTable == IntPtr.Zero) if (fateManager == null)
return IntPtr.Zero; return IntPtr.Zero;
return (IntPtr)this.Struct->Fates[index].Value; return (IntPtr)fateManager->Fates[index].Value;
} }
/// <inheritdoc/> /// <inheritdoc/>

View file

@ -5,9 +5,8 @@ using Dalamud.Game.ClientState.JobGauge.Types;
using Dalamud.IoC; using Dalamud.IoC;
using Dalamud.IoC.Internal; using Dalamud.IoC.Internal;
using Dalamud.Plugin.Services; using Dalamud.Plugin.Services;
using Dalamud.Utility;
using Serilog; using CSJobGaugeManager = FFXIVClientStructs.FFXIV.Client.Game.JobGaugeManager;
namespace Dalamud.Game.ClientState.JobGauge; namespace Dalamud.Game.ClientState.JobGauge;
@ -21,18 +20,15 @@ namespace Dalamud.Game.ClientState.JobGauge;
#pragma warning restore SA1015 #pragma warning restore SA1015
internal class JobGauges : IServiceType, IJobGauges internal class JobGauges : IServiceType, IJobGauges
{ {
private Dictionary<Type, JobGaugeBase> cache = new(); private Dictionary<Type, JobGaugeBase> cache = [];
[ServiceManager.ServiceConstructor] [ServiceManager.ServiceConstructor]
private JobGauges(ClientState clientState) private JobGauges()
{ {
this.Address = clientState.AddressResolver.JobGaugeData;
Log.Verbose($"JobGaugeData address {Util.DescribeAddress(this.Address)}");
} }
/// <inheritdoc/> /// <inheritdoc/>
public IntPtr Address { get; } public unsafe IntPtr Address => (nint)(&CSJobGaugeManager.Instance()->CurrentGauge);
/// <inheritdoc/> /// <inheritdoc/>
public T Get<T>() where T : JobGaugeBase public T Get<T>() where T : JobGaugeBase

View file

@ -7,15 +7,17 @@ using Dalamud.Game.ClientState.Objects.SubKinds;
using Dalamud.Game.ClientState.Objects.Types; using Dalamud.Game.ClientState.Objects.Types;
using Dalamud.IoC; using Dalamud.IoC;
using Dalamud.IoC.Internal; using Dalamud.IoC.Internal;
using Dalamud.Logging.Internal;
using Dalamud.Plugin.Internal; using Dalamud.Plugin.Internal;
using Dalamud.Plugin.Services; using Dalamud.Plugin.Services;
using Dalamud.Utility; using Dalamud.Utility;
using FFXIVClientStructs.Interop;
using Microsoft.Extensions.ObjectPool; using Microsoft.Extensions.ObjectPool;
using Serilog;
using CSGameObject = FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject; using CSGameObject = FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject;
using CSGameObjectManager = FFXIVClientStructs.FFXIV.Client.Game.Object.GameObjectManager;
namespace Dalamud.Game.ClientState.Objects; namespace Dalamud.Game.ClientState.Objects;
@ -29,10 +31,12 @@ namespace Dalamud.Game.ClientState.Objects;
#pragma warning restore SA1015 #pragma warning restore SA1015
internal sealed partial class ObjectTable : IServiceType, IObjectTable internal sealed partial class ObjectTable : IServiceType, IObjectTable
{ {
private const int ObjectTableLength = 599; private static readonly ModuleLog Log = new("ObjectTable");
private static int objectTableLength;
private readonly ClientState clientState; private readonly ClientState clientState;
private readonly CachedEntry[] cachedObjectTable = new CachedEntry[ObjectTableLength]; private readonly CachedEntry[] cachedObjectTable;
private readonly ObjectPool<Enumerator> multiThreadedEnumerators = private readonly ObjectPool<Enumerator> multiThreadedEnumerators =
new DefaultObjectPoolProvider().Create<Enumerator>(); new DefaultObjectPoolProvider().Create<Enumerator>();
@ -46,29 +50,30 @@ internal sealed partial class ObjectTable : IServiceType, IObjectTable
{ {
this.clientState = clientState; this.clientState = clientState;
var nativeObjectTableAddress = (CSGameObject**)this.clientState.AddressResolver.ObjectTable; var nativeObjectTable = CSGameObjectManager.Instance()->Objects.IndexSorted;
objectTableLength = nativeObjectTable.Length;
this.cachedObjectTable = new CachedEntry[objectTableLength];
for (var i = 0; i < this.cachedObjectTable.Length; i++) for (var i = 0; i < this.cachedObjectTable.Length; i++)
this.cachedObjectTable[i] = new(nativeObjectTableAddress, i); this.cachedObjectTable[i] = new(nativeObjectTable.GetPointer(i));
for (var i = 0; i < this.frameworkThreadEnumerators.Length; i++) for (var i = 0; i < this.frameworkThreadEnumerators.Length; i++)
this.frameworkThreadEnumerators[i] = new(this, i); this.frameworkThreadEnumerators[i] = new(this, i);
Log.Verbose($"Object table address {Util.DescribeAddress(this.clientState.AddressResolver.ObjectTable)}");
} }
/// <inheritdoc/> /// <inheritdoc/>
public nint Address public unsafe nint Address
{ {
get get
{ {
_ = this.WarnMultithreadedUsage(); _ = this.WarnMultithreadedUsage();
return this.clientState.AddressResolver.ObjectTable; return (nint)(&CSGameObjectManager.Instance()->Objects);
} }
} }
/// <inheritdoc/> /// <inheritdoc/>
public int Length => ObjectTableLength; public int Length => objectTableLength;
/// <inheritdoc/> /// <inheritdoc/>
public IGameObject? this[int index] public IGameObject? this[int index]
@ -77,7 +82,7 @@ internal sealed partial class ObjectTable : IServiceType, IObjectTable
{ {
_ = this.WarnMultithreadedUsage(); _ = this.WarnMultithreadedUsage();
return index is >= ObjectTableLength or < 0 ? null : this.cachedObjectTable[index].Update(); return (index >= objectTableLength || index < 0) ? null : this.cachedObjectTable[index].Update();
} }
} }
@ -120,7 +125,7 @@ internal sealed partial class ObjectTable : IServiceType, IObjectTable
{ {
_ = this.WarnMultithreadedUsage(); _ = this.WarnMultithreadedUsage();
return index is < 0 or >= ObjectTableLength ? nint.Zero : (nint)this.cachedObjectTable[index].Address; return (index >= objectTableLength || index < 0) ? nint.Zero : (nint)this.cachedObjectTable[index].Address;
} }
/// <inheritdoc/> /// <inheritdoc/>
@ -172,33 +177,21 @@ internal sealed partial class ObjectTable : IServiceType, IObjectTable
} }
/// <summary>Stores an object table entry, with preallocated concrete types.</summary> /// <summary>Stores an object table entry, with preallocated concrete types.</summary>
internal readonly unsafe struct CachedEntry /// <remarks>Initializes a new instance of the <see cref="CachedEntry"/> struct.</remarks>
/// <param name="gameObjectPtr">A pointer to the object table entry this entry should be pointing to.</param>
internal readonly unsafe struct CachedEntry(Pointer<CSGameObject>* gameObjectPtr)
{ {
private readonly CSGameObject** gameObjectPtrPtr; private readonly PlayerCharacter playerCharacter = new(nint.Zero);
private readonly PlayerCharacter playerCharacter; private readonly BattleNpc battleNpc = new(nint.Zero);
private readonly BattleNpc battleNpc; private readonly Npc npc = new(nint.Zero);
private readonly Npc npc; private readonly EventObj eventObj = new(nint.Zero);
private readonly EventObj eventObj; private readonly GameObject gameObject = new(nint.Zero);
private readonly GameObject gameObject;
/// <summary>Initializes a new instance of the <see cref="CachedEntry"/> struct.</summary>
/// <param name="ownerTable">The object table that this entry should be pointing to.</param>
/// <param name="slot">The slot index inside the table.</param>
public CachedEntry(CSGameObject** ownerTable, int slot)
{
this.gameObjectPtrPtr = ownerTable + slot;
this.playerCharacter = new(nint.Zero);
this.battleNpc = new(nint.Zero);
this.npc = new(nint.Zero);
this.eventObj = new(nint.Zero);
this.gameObject = new(nint.Zero);
}
/// <summary>Gets the address of the underlying native object. May be null.</summary> /// <summary>Gets the address of the underlying native object. May be null.</summary>
public CSGameObject* Address public CSGameObject* Address
{ {
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
get => *this.gameObjectPtrPtr; get => gameObjectPtr->Value;
} }
/// <summary>Updates and gets the wrapped game object pointed by this struct.</summary> /// <summary>Updates and gets the wrapped game object pointed by this struct.</summary>
@ -284,11 +277,11 @@ internal sealed partial class ObjectTable
public bool MoveNext() public bool MoveNext()
{ {
if (this.index == ObjectTableLength) if (this.index == objectTableLength)
return false; return false;
var cache = this.owner!.cachedObjectTable.AsSpan(); var cache = this.owner!.cachedObjectTable.AsSpan();
for (this.index++; this.index < ObjectTableLength; this.index++) for (this.index++; this.index < objectTableLength; this.index++)
{ {
if (cache[this.index].Update() is { } ao) if (cache[this.index].Update() is { } ao)
{ {

View file

@ -161,7 +161,7 @@ internal unsafe class Character : GameObject, ICharacter
public byte[] Customize => this.Struct->DrawData.CustomizeData.Data.ToArray(); public byte[] Customize => this.Struct->DrawData.CustomizeData.Data.ToArray();
/// <inheritdoc/> /// <inheritdoc/>
public SeString CompanyTag => MemoryHelper.ReadSeString((nint)Unsafe.AsPointer(ref this.Struct->FreeCompanyTag[0]), 6); public SeString CompanyTag => SeString.Parse(this.Struct->FreeCompanyTag);
/// <summary> /// <summary>
/// Gets the target object ID of the character. /// Gets the target object ID of the character.

View file

@ -197,7 +197,7 @@ internal partial class GameObject
internal unsafe partial class GameObject : IGameObject internal unsafe partial class GameObject : IGameObject
{ {
/// <inheritdoc/> /// <inheritdoc/>
public SeString Name => MemoryHelper.ReadSeString((nint)Unsafe.AsPointer(ref this.Struct->Name[0]), 64); public SeString Name => SeString.Parse(this.Struct->Name);
/// <inheritdoc/> /// <inheritdoc/>
public ulong GameObjectId => this.Struct->GetGameObjectId(); public ulong GameObjectId => this.Struct->GetGameObjectId();

View file

@ -6,9 +6,8 @@ using System.Runtime.InteropServices;
using Dalamud.IoC; using Dalamud.IoC;
using Dalamud.IoC.Internal; using Dalamud.IoC.Internal;
using Dalamud.Plugin.Services; using Dalamud.Plugin.Services;
using Dalamud.Utility;
using Serilog; using CSGroupManager = FFXIVClientStructs.FFXIV.Client.Game.Group.GroupManager;
namespace Dalamud.Game.ClientState.Party; namespace Dalamud.Game.ClientState.Party;
@ -28,14 +27,9 @@ internal sealed unsafe partial class PartyList : IServiceType, IPartyList
[ServiceManager.ServiceDependency] [ServiceManager.ServiceDependency]
private readonly ClientState clientState = Service<ClientState>.Get(); private readonly ClientState clientState = Service<ClientState>.Get();
private readonly ClientStateAddressResolver address;
[ServiceManager.ServiceConstructor] [ServiceManager.ServiceConstructor]
private PartyList() private PartyList()
{ {
this.address = this.clientState.AddressResolver;
Log.Verbose($"Group manager address {Util.DescribeAddress(this.address.GroupManager)}");
} }
/// <inheritdoc/> /// <inheritdoc/>
@ -48,7 +42,7 @@ internal sealed unsafe partial class PartyList : IServiceType, IPartyList
public bool IsAlliance => this.GroupManagerStruct->MainGroup.AllianceFlags > 0; public bool IsAlliance => this.GroupManagerStruct->MainGroup.AllianceFlags > 0;
/// <inheritdoc/> /// <inheritdoc/>
public IntPtr GroupManagerAddress => this.address.GroupManager; public unsafe IntPtr GroupManagerAddress => (nint)CSGroupManager.Instance();
/// <inheritdoc/> /// <inheritdoc/>
public IntPtr GroupListAddress => (IntPtr)Unsafe.AsPointer(ref GroupManagerStruct->MainGroup.PartyMembers[0]); public IntPtr GroupListAddress => (IntPtr)Unsafe.AsPointer(ref GroupManagerStruct->MainGroup.PartyMembers[0]);

View file

@ -181,7 +181,7 @@ internal unsafe class PartyMember : IPartyMember
/// <summary> /// <summary>
/// Gets the displayname of this party member. /// Gets the displayname of this party member.
/// </summary> /// </summary>
public SeString Name => MemoryHelper.ReadSeString((nint)Unsafe.AsPointer(ref Struct->Name[0]), 0x40); public SeString Name => SeString.Parse(this.Struct->Name);
/// <summary> /// <summary>
/// Gets the sex of this party member. /// Gets the sex of this party member.

View file

@ -2,7 +2,6 @@ using System.Collections.Concurrent;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics; using System.Diagnostics;
using System.Linq; using System.Linq;
using System.Runtime.InteropServices;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
@ -16,6 +15,8 @@ using Dalamud.Logging.Internal;
using Dalamud.Plugin.Services; using Dalamud.Plugin.Services;
using Dalamud.Utility; using Dalamud.Utility;
using CSFramework = FFXIVClientStructs.FFXIV.Client.System.Framework.Framework;
namespace Dalamud.Game; namespace Dalamud.Game;
/// <summary> /// <summary>
@ -31,11 +32,9 @@ internal sealed class Framework : IInternalDisposableService, IFramework
private readonly Stopwatch updateStopwatch = new(); private readonly Stopwatch updateStopwatch = new();
private readonly HitchDetector hitchDetector; private readonly HitchDetector hitchDetector;
private readonly Hook<OnUpdateDetour> updateHook; private readonly Hook<CSFramework.Delegates.Tick> updateHook;
private readonly Hook<OnRealDestroyDelegate> destroyHook; private readonly Hook<CSFramework.Delegates.Destroy> destroyHook;
private readonly FrameworkAddressResolver addressResolver;
[ServiceManager.ServiceDependency] [ServiceManager.ServiceDependency]
private readonly GameLifecycle lifecycle = Service<GameLifecycle>.Get(); private readonly GameLifecycle lifecycle = Service<GameLifecycle>.Get();
@ -51,13 +50,10 @@ internal sealed class Framework : IInternalDisposableService, IFramework
private ulong tickCounter; private ulong tickCounter;
[ServiceManager.ServiceConstructor] [ServiceManager.ServiceConstructor]
private Framework(TargetSigScanner sigScanner) private unsafe Framework()
{ {
this.hitchDetector = new HitchDetector("FrameworkUpdate", this.configuration.FrameworkUpdateHitch); this.hitchDetector = new HitchDetector("FrameworkUpdate", this.configuration.FrameworkUpdateHitch);
this.addressResolver = new FrameworkAddressResolver();
this.addressResolver.Setup(sigScanner);
this.frameworkDestroy = new(); this.frameworkDestroy = new();
this.frameworkThreadTaskScheduler = new(); this.frameworkThreadTaskScheduler = new();
this.FrameworkThreadTaskFactory = new( this.FrameworkThreadTaskFactory = new(
@ -66,23 +62,13 @@ internal sealed class Framework : IInternalDisposableService, IFramework
TaskContinuationOptions.None, TaskContinuationOptions.None,
this.frameworkThreadTaskScheduler); this.frameworkThreadTaskScheduler);
this.updateHook = Hook<OnUpdateDetour>.FromAddress(this.addressResolver.TickAddress, this.HandleFrameworkUpdate); this.updateHook = Hook<CSFramework.Delegates.Tick>.FromAddress((nint)CSFramework.StaticVirtualTablePointer->Tick, this.HandleFrameworkUpdate);
this.destroyHook = Hook<OnRealDestroyDelegate>.FromAddress(this.addressResolver.DestroyAddress, this.HandleFrameworkDestroy); this.destroyHook = Hook<CSFramework.Delegates.Destroy>.FromAddress((nint)CSFramework.StaticVirtualTablePointer->Destroy, this.HandleFrameworkDestroy);
this.updateHook.Enable(); this.updateHook.Enable();
this.destroyHook.Enable(); this.destroyHook.Enable();
} }
/// <summary>
/// A delegate type used during the native Framework::destroy.
/// </summary>
/// <param name="framework">The native Framework address.</param>
/// <returns>A value indicating if the call was successful.</returns>
public delegate bool OnRealDestroyDelegate(IntPtr framework);
[UnmanagedFunctionPointer(CallingConvention.ThisCall)]
private delegate bool OnUpdateDetour(IntPtr framework);
/// <inheritdoc/> /// <inheritdoc/>
public event IFramework.OnUpdateDelegate? Update; public event IFramework.OnUpdateDelegate? Update;
@ -390,7 +376,7 @@ internal sealed class Framework : IInternalDisposableService, IFramework
} }
} }
private bool HandleFrameworkUpdate(IntPtr framework) private unsafe bool HandleFrameworkUpdate(CSFramework* thisPtr)
{ {
this.frameworkThreadTaskScheduler.BoundThread ??= Thread.CurrentThread; this.frameworkThreadTaskScheduler.BoundThread ??= Thread.CurrentThread;
@ -483,10 +469,10 @@ internal sealed class Framework : IInternalDisposableService, IFramework
this.hitchDetector.Stop(); this.hitchDetector.Stop();
original: original:
return this.updateHook.OriginalDisposeSafe(framework); return this.updateHook.OriginalDisposeSafe(thisPtr);
} }
private bool HandleFrameworkDestroy(IntPtr framework) private unsafe bool HandleFrameworkDestroy(CSFramework* thisPtr)
{ {
this.frameworkDestroy.Cancel(); this.frameworkDestroy.Cancel();
this.DispatchUpdateEvents = false; this.DispatchUpdateEvents = false;
@ -504,7 +490,7 @@ internal sealed class Framework : IInternalDisposableService, IFramework
ServiceManager.WaitForServiceUnload(); ServiceManager.WaitForServiceUnload();
Log.Information("Framework::Destroy OK!"); Log.Information("Framework::Destroy OK!");
return this.destroyHook.OriginalDisposeSafe(framework); return this.destroyHook.OriginalDisposeSafe(thisPtr);
} }
} }

View file

@ -1,40 +0,0 @@
namespace Dalamud.Game;
/// <summary>
/// The address resolver for the <see cref="Framework"/> class.
/// </summary>
internal sealed class FrameworkAddressResolver : BaseAddressResolver
{
/// <summary>
/// Gets the address for the function that is called once the Framework is destroyed.
/// </summary>
public IntPtr DestroyAddress { get; private set; }
/// <summary>
/// Gets the address for the function that is called once the Framework is free'd.
/// </summary>
public IntPtr FreeAddress { get; private set; }
/// <summary>
/// Gets the function that is called every tick.
/// </summary>
public IntPtr TickAddress { get; private set; }
/// <inheritdoc/>
protected override void Setup64Bit(ISigScanner sig)
{
this.SetupFramework(sig);
}
private void SetupFramework(ISigScanner scanner)
{
this.DestroyAddress =
scanner.ScanText("48 89 5C 24 ?? 48 89 74 24 ?? 57 48 83 EC 20 48 8B 3D ?? ?? ?? ?? 48 8B D9 48 85 FF");
this.FreeAddress =
scanner.ScanText("48 89 5C 24 ?? 48 89 74 24 ?? 57 48 83 EC 20 48 8B D9 48 8B 0D ?? ?? ?? ?? 48 85 C9");
this.TickAddress =
scanner.ScanText("40 53 48 83 EC 20 FF 81 ?? ?? ?? ?? 48 8B D9 48 8D 4C 24 ??");
}
}

View file

@ -11,11 +11,19 @@ using Dalamud.Hooking;
using Dalamud.IoC; using Dalamud.IoC;
using Dalamud.IoC.Internal; using Dalamud.IoC.Internal;
using Dalamud.Logging.Internal; using Dalamud.Logging.Internal;
using Dalamud.Memory;
using Dalamud.Plugin.Services; using Dalamud.Plugin.Services;
using Dalamud.Utility; using Dalamud.Utility;
using FFXIVClientStructs.FFXIV.Client.Game;
using FFXIVClientStructs.FFXIV.Client.System.String; using FFXIVClientStructs.FFXIV.Client.System.String;
using FFXIVClientStructs.FFXIV.Client.UI;
using FFXIVClientStructs.FFXIV.Client.UI.Misc; using FFXIVClientStructs.FFXIV.Client.UI.Misc;
using FFXIVClientStructs.FFXIV.Component.GUI;
using LinkMacroPayloadType = Lumina.Text.Payloads.LinkMacroPayloadType;
using LuminaSeStringBuilder = Lumina.Text.SeStringBuilder;
using ReadOnlySePayloadType = Lumina.Text.ReadOnly.ReadOnlySePayloadType;
using ReadOnlySeStringSpan = Lumina.Text.ReadOnly.ReadOnlySeStringSpan;
namespace Dalamud.Game.Gui; namespace Dalamud.Game.Gui;
@ -27,14 +35,12 @@ internal sealed unsafe class ChatGui : IInternalDisposableService, IChatGui
{ {
private static readonly ModuleLog Log = new("ChatGui"); private static readonly ModuleLog Log = new("ChatGui");
private readonly ChatGuiAddressResolver address;
private readonly Queue<XivChatEntry> chatQueue = new(); private readonly Queue<XivChatEntry> chatQueue = new();
private readonly Dictionary<(string PluginName, uint CommandId), Action<uint, SeString>> dalamudLinkHandlers = new(); private readonly Dictionary<(string PluginName, uint CommandId), Action<uint, SeString>> dalamudLinkHandlers = new();
private readonly Hook<PrintMessageDelegate> printMessageHook; private readonly Hook<PrintMessageDelegate> printMessageHook;
private readonly Hook<PopulateItemLinkDelegate> populateItemLinkHook; private readonly Hook<InventoryItem.Delegates.Copy> inventoryItemCopyHook;
private readonly Hook<InteractableLinkClickedDelegate> interactableLinkClickedHook; private readonly Hook<LogViewer.Delegates.HandleLinkClick> handleLinkClickHook;
[ServiceManager.ServiceDependency] [ServiceManager.ServiceDependency]
private readonly DalamudConfiguration configuration = Service<DalamudConfiguration>.Get(); private readonly DalamudConfiguration configuration = Service<DalamudConfiguration>.Get();
@ -42,29 +48,20 @@ internal sealed unsafe class ChatGui : IInternalDisposableService, IChatGui
private ImmutableDictionary<(string PluginName, uint CommandId), Action<uint, SeString>>? dalamudLinkHandlersCopy; private ImmutableDictionary<(string PluginName, uint CommandId), Action<uint, SeString>>? dalamudLinkHandlersCopy;
[ServiceManager.ServiceConstructor] [ServiceManager.ServiceConstructor]
private ChatGui(TargetSigScanner sigScanner) private ChatGui()
{ {
this.address = new ChatGuiAddressResolver(); this.printMessageHook = Hook<PrintMessageDelegate>.FromAddress(RaptureLogModule.Addresses.PrintMessage.Value, this.HandlePrintMessageDetour);
this.address.Setup(sigScanner); this.inventoryItemCopyHook = Hook<InventoryItem.Delegates.Copy>.FromAddress(InventoryItem.Addresses.Copy.Value, this.InventoryItemCopyDetour);
this.handleLinkClickHook = Hook<LogViewer.Delegates.HandleLinkClick>.FromAddress(LogViewer.Addresses.HandleLinkClick.Value, this.HandleLinkClickDetour);
this.printMessageHook = Hook<PrintMessageDelegate>.FromAddress((nint)RaptureLogModule.Addresses.PrintMessage.Value, this.HandlePrintMessageDetour);
this.populateItemLinkHook = Hook<PopulateItemLinkDelegate>.FromAddress(this.address.PopulateItemLinkObject, this.HandlePopulateItemLinkDetour);
this.interactableLinkClickedHook = Hook<InteractableLinkClickedDelegate>.FromAddress(this.address.InteractableLinkClicked, this.InteractableLinkClickedDetour);
this.printMessageHook.Enable(); this.printMessageHook.Enable();
this.populateItemLinkHook.Enable(); this.inventoryItemCopyHook.Enable();
this.interactableLinkClickedHook.Enable(); this.handleLinkClickHook.Enable();
} }
[UnmanagedFunctionPointer(CallingConvention.ThisCall)] [UnmanagedFunctionPointer(CallingConvention.ThisCall)]
private delegate uint PrintMessageDelegate(RaptureLogModule* manager, XivChatType chatType, Utf8String* sender, Utf8String* message, int timestamp, byte silent); private delegate uint PrintMessageDelegate(RaptureLogModule* manager, XivChatType chatType, Utf8String* sender, Utf8String* message, int timestamp, byte silent);
[UnmanagedFunctionPointer(CallingConvention.ThisCall)]
private delegate void PopulateItemLinkDelegate(IntPtr linkObjectPtr, IntPtr itemInfoPtr);
[UnmanagedFunctionPointer(CallingConvention.ThisCall)]
private delegate void InteractableLinkClickedDelegate(IntPtr managerPtr, IntPtr messagePtr);
/// <inheritdoc/> /// <inheritdoc/>
public event IChatGui.OnMessageDelegate? ChatMessage; public event IChatGui.OnMessageDelegate? ChatMessage;
@ -78,7 +75,7 @@ internal sealed unsafe class ChatGui : IInternalDisposableService, IChatGui
public event IChatGui.OnMessageUnhandledDelegate? ChatMessageUnhandled; public event IChatGui.OnMessageUnhandledDelegate? ChatMessageUnhandled;
/// <inheritdoc/> /// <inheritdoc/>
public int LastLinkedItemId { get; private set; } public uint LastLinkedItemId { get; private set; }
/// <inheritdoc/> /// <inheritdoc/>
public byte LastLinkedItemFlags { get; private set; } public byte LastLinkedItemFlags { get; private set; }
@ -106,8 +103,8 @@ internal sealed unsafe class ChatGui : IInternalDisposableService, IChatGui
void IInternalDisposableService.DisposeService() void IInternalDisposableService.DisposeService()
{ {
this.printMessageHook.Dispose(); this.printMessageHook.Dispose();
this.populateItemLinkHook.Dispose(); this.inventoryItemCopyHook.Dispose();
this.interactableLinkClickedHook.Dispose(); this.handleLinkClickHook.Dispose();
} }
/// <inheritdoc/> /// <inheritdoc/>
@ -275,21 +272,20 @@ internal sealed unsafe class ChatGui : IInternalDisposableService, IChatGui
}); });
} }
private void HandlePopulateItemLinkDetour(IntPtr linkObjectPtr, IntPtr itemInfoPtr) private void InventoryItemCopyDetour(InventoryItem* thisPtr, InventoryItem* otherPtr)
{ {
this.inventoryItemCopyHook.Original(thisPtr, otherPtr);
try try
{ {
this.populateItemLinkHook.Original(linkObjectPtr, itemInfoPtr); this.LastLinkedItemId = otherPtr->ItemId;
this.LastLinkedItemFlags = (byte)otherPtr->Flags;
this.LastLinkedItemId = Marshal.ReadInt32(itemInfoPtr, 8); // Log.Verbose($"InventoryItemCopyDetour {thisPtr} {otherPtr} - linked:{this.LastLinkedItemId}");
this.LastLinkedItemFlags = Marshal.ReadByte(itemInfoPtr, 0x14);
// Log.Verbose($"HandlePopulateItemLinkDetour {linkObjectPtr} {itemInfoPtr} - linked:{this.LastLinkedItemId}");
} }
catch (Exception ex) catch (Exception ex)
{ {
Log.Error(ex, "Exception onPopulateItemLink hook."); Log.Error(ex, "Exception in InventoryItemCopyHook");
this.populateItemLinkHook.Original(linkObjectPtr, itemInfoPtr);
} }
} }
@ -299,58 +295,57 @@ internal sealed unsafe class ChatGui : IInternalDisposableService, IChatGui
try try
{ {
var originalSenderData = sender->AsSpan().ToArray(); var parsedSender = SeString.Parse(sender->AsSpan());
var originalMessageData = message->AsSpan().ToArray(); var parsedMessage = SeString.Parse(message->AsSpan());
var parsedSender = SeString.Parse(originalSenderData); var terminatedSender = parsedSender.EncodeWithNullTerminator();
var parsedMessage = SeString.Parse(originalMessageData); var terminatedMessage = parsedMessage.EncodeWithNullTerminator();
// Call events // Call events
var isHandled = false; var isHandled = false;
var invocationList = this.CheckMessageHandled!.GetInvocationList(); if (this.CheckMessageHandled is { } handledCallback)
foreach (var @delegate in invocationList)
{ {
try foreach (var action in handledCallback.GetInvocationList().Cast<IChatGui.OnCheckMessageHandledDelegate>())
{
var messageHandledDelegate = @delegate as IChatGui.OnCheckMessageHandledDelegate;
messageHandledDelegate!.Invoke(chatType, timestamp, ref parsedSender, ref parsedMessage, ref isHandled);
}
catch (Exception e)
{
Log.Error(e, "Could not invoke registered OnCheckMessageHandledDelegate for {Name}", @delegate.Method.Name);
}
}
if (!isHandled)
{
invocationList = this.ChatMessage!.GetInvocationList();
foreach (var @delegate in invocationList)
{ {
try try
{ {
var messageHandledDelegate = @delegate as IChatGui.OnMessageDelegate; action(chatType, timestamp, ref parsedSender, ref parsedMessage, ref isHandled);
messageHandledDelegate!.Invoke(chatType, timestamp, ref parsedSender, ref parsedMessage, ref isHandled);
} }
catch (Exception e) catch (Exception e)
{ {
Log.Error(e, "Could not invoke registered OnMessageDelegate for {Name}", @delegate.Method.Name); Log.Error(e, "Could not invoke registered OnCheckMessageHandledDelegate for {Name}", action.Method);
} }
} }
} }
var possiblyModifiedSenderData = parsedSender.Encode(); if (!isHandled && this.ChatMessage is { } callback)
var possiblyModifiedMessageData = parsedMessage.Encode();
if (!Util.FastByteArrayCompare(originalSenderData, possiblyModifiedSenderData))
{ {
Log.Verbose($"HandlePrintMessageDetour Sender modified: {SeString.Parse(originalSenderData)} -> {parsedSender}"); foreach (var action in callback.GetInvocationList().Cast<IChatGui.OnMessageDelegate>())
{
try
{
action(chatType, timestamp, ref parsedSender, ref parsedMessage, ref isHandled);
}
catch (Exception e)
{
Log.Error(e, "Could not invoke registered OnMessageDelegate for {Name}", action.Method);
}
}
}
var possiblyModifiedSenderData = parsedSender.EncodeWithNullTerminator();
var possiblyModifiedMessageData = parsedMessage.EncodeWithNullTerminator();
if (!terminatedSender.SequenceEqual(possiblyModifiedSenderData))
{
Log.Verbose($"HandlePrintMessageDetour Sender modified: {SeString.Parse(terminatedSender)} -> {parsedSender}");
sender->SetString(possiblyModifiedSenderData); sender->SetString(possiblyModifiedSenderData);
} }
if (!Util.FastByteArrayCompare(originalMessageData, possiblyModifiedMessageData)) if (!terminatedMessage.SequenceEqual(possiblyModifiedMessageData))
{ {
Log.Verbose($"HandlePrintMessageDetour Message modified: {SeString.Parse(originalMessageData)} -> {parsedMessage}"); Log.Verbose($"HandlePrintMessageDetour Message modified: {SeString.Parse(terminatedMessage)} -> {parsedMessage}");
message->SetString(possiblyModifiedMessageData); message->SetString(possiblyModifiedMessageData);
} }
@ -374,42 +369,57 @@ internal sealed unsafe class ChatGui : IInternalDisposableService, IChatGui
return messageId; return messageId;
} }
private void InteractableLinkClickedDetour(IntPtr managerPtr, IntPtr messagePtr) private void HandleLinkClickDetour(LogViewer* thisPtr, LinkData* linkData)
{ {
if ((Payload.EmbeddedInfoType)(linkData->LinkType + 1) != Payload.EmbeddedInfoType.DalamudLink)
{
this.handleLinkClickHook.Original(thisPtr, linkData);
return;
}
Log.Verbose($"InteractableLinkClicked: {Payload.EmbeddedInfoType.DalamudLink}");
var sb = LuminaSeStringBuilder.SharedPool.Get();
try try
{ {
var interactableType = (Payload.EmbeddedInfoType)(Marshal.ReadByte(messagePtr, 0x1B) + 1); var seStringSpan = new ReadOnlySeStringSpan(linkData->Payload);
if (interactableType != Payload.EmbeddedInfoType.DalamudLink) // read until link terminator
foreach (var payload in seStringSpan)
{ {
this.interactableLinkClickedHook.Original(managerPtr, messagePtr); sb.Append(payload);
return;
if (payload.Type == ReadOnlySePayloadType.Macro &&
payload.MacroCode == Lumina.Text.Payloads.MacroCode.Link &&
payload.TryGetExpression(out var expr1) &&
expr1.TryGetInt(out var expr1Val) &&
expr1Val == (int)LinkMacroPayloadType.Terminator)
{
break;
}
} }
Log.Verbose($"InteractableLinkClicked: {Payload.EmbeddedInfoType.DalamudLink}"); var seStr = SeString.Parse(sb.ToArray());
if (seStr.Payloads.Count == 0 || seStr.Payloads[0] is not DalamudLinkPayload link)
return;
var payloadPtr = Marshal.ReadIntPtr(messagePtr, 0x10); if (this.RegisteredLinkHandlers.TryGetValue((link.Plugin, link.CommandId), out var value))
var seStr = MemoryHelper.ReadSeStringNullTerminated(payloadPtr);
var terminatorIndex = seStr.Payloads.IndexOf(RawPayload.LinkTerminator);
var payloads = terminatorIndex >= 0 ? seStr.Payloads.Take(terminatorIndex + 1).ToList() : seStr.Payloads;
if (payloads.Count == 0) return;
var linkPayload = payloads[0];
if (linkPayload is DalamudLinkPayload link)
{ {
if (this.RegisteredLinkHandlers.TryGetValue((link.Plugin, link.CommandId), out var value)) Log.Verbose($"Sending DalamudLink to {link.Plugin}: {link.CommandId}");
{ value.Invoke(link.CommandId, seStr);
Log.Verbose($"Sending DalamudLink to {link.Plugin}: {link.CommandId}"); }
value.Invoke(link.CommandId, new SeString(payloads)); else
} {
else Log.Debug($"No DalamudLink registered for {link.Plugin} with ID of {link.CommandId}");
{
Log.Debug($"No DalamudLink registered for {link.Plugin} with ID of {link.CommandId}");
}
} }
} }
catch (Exception ex) catch (Exception ex)
{ {
Log.Error(ex, "Exception on InteractableLinkClicked hook"); Log.Error(ex, "Exception in HandleLinkClickDetour");
}
finally
{
LuminaSeStringBuilder.SharedPool.Return(sb);
} }
} }
} }
@ -451,7 +461,7 @@ internal class ChatGuiPluginScoped : IInternalDisposableService, IChatGui
public event IChatGui.OnMessageUnhandledDelegate? ChatMessageUnhandled; public event IChatGui.OnMessageUnhandledDelegate? ChatMessageUnhandled;
/// <inheritdoc/> /// <inheritdoc/>
public int LastLinkedItemId => this.chatGuiService.LastLinkedItemId; public uint LastLinkedItemId => this.chatGuiService.LastLinkedItemId;
/// <inheritdoc/> /// <inheritdoc/>
public byte LastLinkedItemFlags => this.chatGuiService.LastLinkedItemFlags; public byte LastLinkedItemFlags => this.chatGuiService.LastLinkedItemFlags;

View file

@ -1,28 +0,0 @@
namespace Dalamud.Game.Gui;
/// <summary>
/// The address resolver for the <see cref="ChatGui"/> class.
/// </summary>
internal sealed class ChatGuiAddressResolver : BaseAddressResolver
{
/// <summary>
/// Gets the address of the native PopulateItemLinkObject method.
/// </summary>
public IntPtr PopulateItemLinkObject { get; private set; }
/// <summary>
/// Gets the address of the native InteractableLinkClicked method.
/// </summary>
public IntPtr InteractableLinkClicked { get; private set; }
/// <inheritdoc/>
protected override void Setup64Bit(ISigScanner sig)
{
// PopulateItemLinkObject = sig.ScanText("48 89 5C 24 08 57 48 83 EC 20 80 7A 06 00 48 8B DA 48 8B F9 74 14 48 8B CA E8 32 03 00 00 48 8B C8 E8 FA F2 B0 FF 8B C8 EB 1D 0F B6 42 14 8B 4A");
// PopulateItemLinkObject = sig.ScanText( "48 89 5C 24 08 57 48 83 EC 20 80 7A 06 00 48 8B DA 48 8B F9 74 14 48 8B CA E8 32 03 00 00 48 8B C8 E8 ?? ?? B0 FF 8B C8 EB 1D 0F B6 42 14 8B 4A"); 5.0
this.PopulateItemLinkObject = sig.ScanText("E8 ?? ?? ?? ?? 8B 4E FC");
this.InteractableLinkClicked = sig.ScanText("E8 ?? ?? ?? ?? 48 8B 4B ?? E8 ?? ?? ?? ?? 33 D2");
}
}

View file

@ -1,3 +1,4 @@
using System.Linq;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using System.Threading.Tasks; using System.Threading.Tasks;
@ -8,6 +9,9 @@ using Dalamud.IoC.Internal;
using Dalamud.Memory; using Dalamud.Memory;
using Dalamud.Plugin.Services; using Dalamud.Plugin.Services;
using FFXIVClientStructs.FFXIV.Client.UI;
using FFXIVClientStructs.FFXIV.Component.GUI;
using Serilog; using Serilog;
namespace Dalamud.Game.Gui.FlyText; namespace Dalamud.Game.Gui.FlyText;
@ -29,7 +33,7 @@ internal sealed class FlyTextGui : IInternalDisposableService, IFlyTextGui
private readonly Hook<CreateFlyTextDelegate> createFlyTextHook; private readonly Hook<CreateFlyTextDelegate> createFlyTextHook;
[ServiceManager.ServiceConstructor] [ServiceManager.ServiceConstructor]
private FlyTextGui(TargetSigScanner sigScanner) private unsafe FlyTextGui(TargetSigScanner sigScanner)
{ {
this.Address = new FlyTextGuiAddressResolver(); this.Address = new FlyTextGuiAddressResolver();
this.Address.Setup(sigScanner); this.Address.Setup(sigScanner);
@ -43,29 +47,29 @@ internal sealed class FlyTextGui : IInternalDisposableService, IFlyTextGui
/// <summary> /// <summary>
/// Private delegate for the native CreateFlyText function's hook. /// Private delegate for the native CreateFlyText function's hook.
/// </summary> /// </summary>
private delegate IntPtr CreateFlyTextDelegate( private unsafe delegate nint CreateFlyTextDelegate(
IntPtr addonFlyText, AtkUnitBase* thisPtr,
FlyTextKind kind, FlyTextKind kind,
int val1, int val1,
int val2, int val2,
IntPtr text2, byte* text2,
uint color, uint color,
uint icon, uint icon,
uint damageTypeIcon, uint damageTypeIcon,
IntPtr text1, byte* text1,
float yOffset); float yOffset);
/// <summary> /// <summary>
/// Private delegate for the native AddFlyText function pointer. /// Private delegate for the native AddFlyText function pointer.
/// </summary> /// </summary>
private delegate void AddFlyTextDelegate( private unsafe delegate void AddFlyTextDelegate(
IntPtr addonFlyText, AtkUnitBase* thisPtr,
uint actorIndex, uint actorIndex,
uint messageMax, uint messageMax,
IntPtr numbers, NumberArrayData* numberArrayData,
uint offsetNum, uint offsetNum,
uint offsetNumMax, uint offsetNumMax,
IntPtr strings, StringArrayData* stringArrayData,
uint offsetStr, uint offsetStr,
uint offsetStrMax, uint offsetStrMax,
int unknown); int unknown);
@ -87,26 +91,16 @@ internal sealed class FlyTextGui : IInternalDisposableService, IFlyTextGui
public unsafe void AddFlyText(FlyTextKind kind, uint actorIndex, uint val1, uint val2, SeString text1, SeString text2, uint color, uint icon, uint damageTypeIcon) public unsafe void AddFlyText(FlyTextKind kind, uint actorIndex, uint val1, uint val2, SeString text1, SeString text2, uint color, uint icon, uint damageTypeIcon)
{ {
// Known valid flytext region within the atk arrays // Known valid flytext region within the atk arrays
var numIndex = 30;
var strIndex = 27;
var numOffset = 161u; var numOffset = 161u;
var strOffset = 28u; var strOffset = 28u;
// Get the UI module and flytext addon pointers var flytext = RaptureAtkUnitManager.Instance()->GetAddonByName("_FlyText");
var gameGui = Service<GameGui>.GetNullable(); if (flytext == null)
if (gameGui == null)
return;
var ui = (FFXIVClientStructs.FFXIV.Client.UI.UIModule*)gameGui.GetUIModule();
var flytext = gameGui.GetAddonByName("_FlyText");
if (ui == null || flytext == IntPtr.Zero)
return; return;
// Get the number and string arrays we need // Get the number and string arrays we need
var atkArrayDataHolder = ui->GetRaptureAtkModule()->AtkModule.AtkArrayDataHolder; var numArray = AtkStage.Instance()->GetNumberArrayData(NumberArrayType.FlyText);
var numArray = atkArrayDataHolder._NumberArrays[numIndex]; var strArray = AtkStage.Instance()->GetStringArrayData(StringArrayType.FlyText);
var strArray = atkArrayDataHolder._StringArrays[strIndex];
// Write the values to the arrays using a known valid flytext region // Write the values to the arrays using a known valid flytext region
numArray->IntArray[numOffset + 0] = 1; // Some kind of "Enabled" flag for this section numArray->IntArray[numOffset + 0] = 1; // Some kind of "Enabled" flag for this section
@ -120,44 +114,35 @@ internal sealed class FlyTextGui : IInternalDisposableService, IFlyTextGui
numArray->IntArray[numOffset + 8] = 0; // Unknown numArray->IntArray[numOffset + 8] = 0; // Unknown
numArray->IntArray[numOffset + 9] = 0; // Unknown, has something to do with yOffset numArray->IntArray[numOffset + 9] = 0; // Unknown, has something to do with yOffset
strArray->SetValue((int)strOffset + 0, text1.Encode(), false, true, false); strArray->SetValue((int)strOffset + 0, text1.EncodeWithNullTerminator(), false, true, false);
strArray->SetValue((int)strOffset + 1, text2.Encode(), false, true, false); strArray->SetValue((int)strOffset + 1, text2.EncodeWithNullTerminator(), false, true, false);
this.addFlyTextNative( this.addFlyTextNative(
flytext, flytext,
actorIndex, actorIndex,
1, 1,
(IntPtr)numArray, numArray,
numOffset, numOffset,
10, 10,
(IntPtr)strArray, strArray,
strOffset, strOffset,
2, 2,
0); 0);
} }
private static byte[] Terminate(byte[] source) private unsafe nint CreateFlyTextDetour(
{ AtkUnitBase* thisPtr,
var terminated = new byte[source.Length + 1];
Array.Copy(source, 0, terminated, 0, source.Length);
terminated[^1] = 0;
return terminated;
}
private IntPtr CreateFlyTextDetour(
IntPtr addonFlyText,
FlyTextKind kind, FlyTextKind kind,
int val1, int val1,
int val2, int val2,
IntPtr text2, byte* text2,
uint color, uint color,
uint icon, uint icon,
uint damageTypeIcon, uint damageTypeIcon,
IntPtr text1, byte* text1,
float yOffset) float yOffset)
{ {
var retVal = IntPtr.Zero; var retVal = nint.Zero;
try try
{ {
Log.Verbose("[FlyText] Enter CreateFlyText detour!"); Log.Verbose("[FlyText] Enter CreateFlyText detour!");
@ -167,19 +152,19 @@ internal sealed class FlyTextGui : IInternalDisposableService, IFlyTextGui
var tmpKind = kind; var tmpKind = kind;
var tmpVal1 = val1; var tmpVal1 = val1;
var tmpVal2 = val2; var tmpVal2 = val2;
var tmpText1 = text1 == IntPtr.Zero ? string.Empty : MemoryHelper.ReadSeStringNullTerminated(text1); var tmpText1 = text1 == null ? string.Empty : MemoryHelper.ReadSeStringNullTerminated((nint)text1);
var tmpText2 = text2 == IntPtr.Zero ? string.Empty : MemoryHelper.ReadSeStringNullTerminated(text2); var tmpText2 = text2 == null ? string.Empty : MemoryHelper.ReadSeStringNullTerminated((nint)text2);
var tmpColor = color; var tmpColor = color;
var tmpIcon = icon; var tmpIcon = icon;
var tmpDamageTypeIcon = damageTypeIcon; var tmpDamageTypeIcon = damageTypeIcon;
var tmpYOffset = yOffset; var tmpYOffset = yOffset;
var cmpText1 = tmpText1.ToString(); var originalText1 = tmpText1.EncodeWithNullTerminator();
var cmpText2 = tmpText2.ToString(); var originalText2 = tmpText2.EncodeWithNullTerminator();
Log.Verbose($"[FlyText] Called with addonFlyText({addonFlyText.ToInt64():X}) " + Log.Verbose($"[FlyText] Called with addonFlyText({(nint)thisPtr:X}) " +
$"kind({kind}) val1({val1}) val2({val2}) damageTypeIcon({damageTypeIcon}) " + $"kind({kind}) val1({val1}) val2({val2}) damageTypeIcon({damageTypeIcon}) " +
$"text1({text1.ToInt64():X}, \"{tmpText1}\") text2({text2.ToInt64():X}, \"{tmpText2}\") " + $"text1({(nint)text1:X}, \"{tmpText1}\") text2({(nint)text2:X}, \"{tmpText2}\") " +
$"color({color:X}) icon({icon}) yOffset({yOffset})"); $"color({color:X}) icon({icon}) yOffset({yOffset})");
Log.Verbose("[FlyText] Calling flytext events!"); Log.Verbose("[FlyText] Calling flytext events!");
this.FlyTextCreated?.Invoke( this.FlyTextCreated?.Invoke(
@ -204,12 +189,15 @@ internal sealed class FlyTextGui : IInternalDisposableService, IFlyTextGui
return IntPtr.Zero; return IntPtr.Zero;
} }
var maybeModifiedText1 = tmpText1.EncodeWithNullTerminator();
var maybeModifiedText2 = tmpText2.EncodeWithNullTerminator();
// Check if any values have changed // Check if any values have changed
var dirty = tmpKind != kind || var dirty = tmpKind != kind ||
tmpVal1 != val1 || tmpVal1 != val1 ||
tmpVal2 != val2 || tmpVal2 != val2 ||
tmpText1.ToString() != cmpText1 || !maybeModifiedText1.SequenceEqual(originalText1) ||
tmpText2.ToString() != cmpText2 || !maybeModifiedText2.SequenceEqual(originalText2) ||
tmpDamageTypeIcon != damageTypeIcon || tmpDamageTypeIcon != damageTypeIcon ||
tmpColor != color || tmpColor != color ||
tmpIcon != icon || tmpIcon != icon ||
@ -219,28 +207,26 @@ internal sealed class FlyTextGui : IInternalDisposableService, IFlyTextGui
if (!dirty) if (!dirty)
{ {
Log.Verbose("[FlyText] Calling flytext with original args."); Log.Verbose("[FlyText] Calling flytext with original args.");
return this.createFlyTextHook.Original(addonFlyText, kind, val1, val2, text2, color, icon, return this.createFlyTextHook.Original(thisPtr, kind, val1, val2, text2, color, icon,
damageTypeIcon, text1, yOffset); damageTypeIcon, text1, yOffset);
} }
var terminated1 = Terminate(tmpText1.Encode()); var pText1 = Marshal.AllocHGlobal(maybeModifiedText1.Length);
var terminated2 = Terminate(tmpText2.Encode()); var pText2 = Marshal.AllocHGlobal(maybeModifiedText2.Length);
var pText1 = Marshal.AllocHGlobal(terminated1.Length); Marshal.Copy(maybeModifiedText1, 0, pText1, maybeModifiedText1.Length);
var pText2 = Marshal.AllocHGlobal(terminated2.Length); Marshal.Copy(maybeModifiedText2, 0, pText2, maybeModifiedText2.Length);
Marshal.Copy(terminated1, 0, pText1, terminated1.Length);
Marshal.Copy(terminated2, 0, pText2, terminated2.Length);
Log.Verbose("[FlyText] Allocated and set strings."); Log.Verbose("[FlyText] Allocated and set strings.");
retVal = this.createFlyTextHook.Original( retVal = this.createFlyTextHook.Original(
addonFlyText, thisPtr,
tmpKind, tmpKind,
tmpVal1, tmpVal1,
tmpVal2, tmpVal2,
pText2, (byte*)pText2,
tmpColor, tmpColor,
tmpIcon, tmpIcon,
tmpDamageTypeIcon, tmpDamageTypeIcon,
pText1, (byte*)pText1,
tmpYOffset); tmpYOffset);
Log.Verbose("[FlyText] Returned from original. Delaying free task."); Log.Verbose("[FlyText] Returned from original. Delaying free task.");

View file

@ -1,4 +1,3 @@
using System.Numerics;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using Dalamud.Game.Text.SeStringHandling.Payloads; using Dalamud.Game.Text.SeStringHandling.Payloads;
@ -18,7 +17,6 @@ using FFXIVClientStructs.FFXIV.Client.UI.Agent;
using FFXIVClientStructs.FFXIV.Common.Component.BGCollision; using FFXIVClientStructs.FFXIV.Common.Component.BGCollision;
using FFXIVClientStructs.FFXIV.Component.GUI; using FFXIVClientStructs.FFXIV.Component.GUI;
using ImGuiNET; using ImGuiNET;
using SharpDX;
using Vector2 = System.Numerics.Vector2; using Vector2 = System.Numerics.Vector2;
using Vector3 = System.Numerics.Vector3; using Vector3 = System.Numerics.Vector3;
@ -33,21 +31,19 @@ namespace Dalamud.Game.Gui;
internal sealed unsafe class GameGui : IInternalDisposableService, IGameGui internal sealed unsafe class GameGui : IInternalDisposableService, IGameGui
{ {
private static readonly ModuleLog Log = new("GameGui"); private static readonly ModuleLog Log = new("GameGui");
[ServiceManager.ServiceDependency]
private readonly Framework framework = Service<Framework>.Get();
private readonly GameGuiAddressResolver address; private readonly GameGuiAddressResolver address;
private readonly Hook<SetGlobalBgmDelegate> setGlobalBgmHook; private readonly Hook<SetGlobalBgmDelegate> setGlobalBgmHook;
private readonly Hook<HandleItemHoverDelegate> handleItemHoverHook;
private readonly Hook<HandleItemOutDelegate> handleItemOutHook;
private readonly Hook<HandleActionHoverDelegate> handleActionHoverHook; private readonly Hook<HandleActionHoverDelegate> handleActionHoverHook;
private readonly Hook<HandleActionOutDelegate> handleActionOutHook; private readonly Hook<HandleActionOutDelegate> handleActionOutHook;
private readonly Hook<HandleImmDelegate> handleImmHook; private readonly Hook<HandleImmDelegate> handleImmHook;
private readonly Hook<ToggleUiHideDelegate> toggleUiHideHook; private readonly Hook<RaptureAtkModule.Delegates.SetUiVisibility> setUiVisibilityHook;
private readonly Hook<Utf8StringFromSequenceDelegate> utf8StringFromSequenceHook; private readonly Hook<Utf8StringFromSequenceDelegate> utf8StringFromSequenceHook;
private GetUIMapObjectDelegate? getUIMapObject;
private OpenMapWithFlagDelegate? openMapWithFlag;
[ServiceManager.ServiceConstructor] [ServiceManager.ServiceConstructor]
private GameGui(TargetSigScanner sigScanner) private GameGui(TargetSigScanner sigScanner)
{ {
@ -57,32 +53,27 @@ internal sealed unsafe class GameGui : IInternalDisposableService, IGameGui
Log.Verbose("===== G A M E G U I ====="); Log.Verbose("===== G A M E G U I =====");
Log.Verbose($"GameGuiManager address {Util.DescribeAddress(this.address.BaseAddress)}"); Log.Verbose($"GameGuiManager address {Util.DescribeAddress(this.address.BaseAddress)}");
Log.Verbose($"SetGlobalBgm address {Util.DescribeAddress(this.address.SetGlobalBgm)}"); Log.Verbose($"SetGlobalBgm address {Util.DescribeAddress(this.address.SetGlobalBgm)}");
Log.Verbose($"HandleItemHover address {Util.DescribeAddress(this.address.HandleItemHover)}");
Log.Verbose($"HandleItemOut address {Util.DescribeAddress(this.address.HandleItemOut)}");
Log.Verbose($"HandleImm address {Util.DescribeAddress(this.address.HandleImm)}"); Log.Verbose($"HandleImm address {Util.DescribeAddress(this.address.HandleImm)}");
this.setGlobalBgmHook = Hook<SetGlobalBgmDelegate>.FromAddress(this.address.SetGlobalBgm, this.HandleSetGlobalBgmDetour); this.setGlobalBgmHook = Hook<SetGlobalBgmDelegate>.FromAddress(this.address.SetGlobalBgm, this.HandleSetGlobalBgmDetour);
this.handleItemHoverHook = Hook<HandleItemHoverDelegate>.FromAddress(this.address.HandleItemHover, this.HandleItemHoverDetour);
this.handleItemOutHook = Hook<HandleItemOutDelegate>.FromAddress(this.address.HandleItemOut, this.HandleItemOutDetour);
this.handleActionHoverHook = Hook<HandleActionHoverDelegate>.FromAddress(this.address.HandleActionHover, this.HandleActionHoverDetour); this.handleActionHoverHook = Hook<HandleActionHoverDelegate>.FromAddress(this.address.HandleActionHover, this.HandleActionHoverDetour);
this.handleActionOutHook = Hook<HandleActionOutDelegate>.FromAddress(this.address.HandleActionOut, this.HandleActionOutDetour); this.handleActionOutHook = Hook<HandleActionOutDelegate>.FromAddress(this.address.HandleActionOut, this.HandleActionOutDetour);
this.handleImmHook = Hook<HandleImmDelegate>.FromAddress(this.address.HandleImm, this.HandleImmDetour); this.handleImmHook = Hook<HandleImmDelegate>.FromAddress(this.address.HandleImm, this.HandleImmDetour);
this.toggleUiHideHook = Hook<ToggleUiHideDelegate>.FromAddress(this.address.ToggleUiHide, this.ToggleUiHideDetour); this.setUiVisibilityHook = Hook<RaptureAtkModule.Delegates.SetUiVisibility>.FromAddress((nint)RaptureAtkModule.StaticVirtualTablePointer->SetUiVisibility, this.SetUiVisibilityDetour);
this.utf8StringFromSequenceHook = Hook<Utf8StringFromSequenceDelegate>.FromAddress(this.address.Utf8StringFromSequence, this.Utf8StringFromSequenceDetour); this.utf8StringFromSequenceHook = Hook<Utf8StringFromSequenceDelegate>.FromAddress(this.address.Utf8StringFromSequence, this.Utf8StringFromSequenceDetour);
this.setGlobalBgmHook.Enable(); this.setGlobalBgmHook.Enable();
this.handleItemHoverHook.Enable();
this.handleItemOutHook.Enable();
this.handleImmHook.Enable(); this.handleImmHook.Enable();
this.toggleUiHideHook.Enable(); this.setUiVisibilityHook.Enable();
this.handleActionHoverHook.Enable(); this.handleActionHoverHook.Enable();
this.handleActionOutHook.Enable(); this.handleActionOutHook.Enable();
this.utf8StringFromSequenceHook.Enable(); this.utf8StringFromSequenceHook.Enable();
this.framework.Update += this.FrameworkUpdate;
} }
// Hooked delegates // Hooked delegates
@ -90,21 +81,9 @@ internal sealed unsafe class GameGui : IInternalDisposableService, IGameGui
[UnmanagedFunctionPointer(CallingConvention.ThisCall)] [UnmanagedFunctionPointer(CallingConvention.ThisCall)]
private delegate Utf8String* Utf8StringFromSequenceDelegate(Utf8String* thisPtr, byte* sourcePtr, nuint sourceLen); private delegate Utf8String* Utf8StringFromSequenceDelegate(Utf8String* thisPtr, byte* sourcePtr, nuint sourceLen);
[UnmanagedFunctionPointer(CallingConvention.ThisCall)]
private delegate IntPtr GetUIMapObjectDelegate(IntPtr uiObject);
[UnmanagedFunctionPointer(CallingConvention.ThisCall, CharSet = CharSet.Ansi)]
private delegate bool OpenMapWithFlagDelegate(IntPtr uiMapObject, string flag);
[UnmanagedFunctionPointer(CallingConvention.ThisCall)] [UnmanagedFunctionPointer(CallingConvention.ThisCall)]
private delegate IntPtr SetGlobalBgmDelegate(ushort bgmKey, byte a2, uint a3, uint a4, uint a5, byte a6); private delegate IntPtr SetGlobalBgmDelegate(ushort bgmKey, byte a2, uint a3, uint a4, uint a5, byte a6);
[UnmanagedFunctionPointer(CallingConvention.ThisCall)]
private delegate IntPtr HandleItemHoverDelegate(IntPtr hoverState, IntPtr a2, IntPtr a3, ulong a4);
[UnmanagedFunctionPointer(CallingConvention.ThisCall)]
private delegate IntPtr HandleItemOutDelegate(IntPtr hoverState, IntPtr a2, IntPtr a3, ulong a4);
[UnmanagedFunctionPointer(CallingConvention.ThisCall)] [UnmanagedFunctionPointer(CallingConvention.ThisCall)]
private delegate void HandleActionHoverDelegate(IntPtr hoverState, HoverActionKind a2, uint a3, int a4, byte a5); private delegate void HandleActionHoverDelegate(IntPtr hoverState, HoverActionKind a2, uint a3, int a4, byte a5);
@ -113,9 +92,6 @@ internal sealed unsafe class GameGui : IInternalDisposableService, IGameGui
[UnmanagedFunctionPointer(CallingConvention.ThisCall)] [UnmanagedFunctionPointer(CallingConvention.ThisCall)]
private delegate char HandleImmDelegate(IntPtr framework, char a2, byte a3); private delegate char HandleImmDelegate(IntPtr framework, char a2, byte a3);
[UnmanagedFunctionPointer(CallingConvention.ThisCall)]
private delegate IntPtr ToggleUiHideDelegate(IntPtr thisPtr, bool uiVisible);
/// <inheritdoc/> /// <inheritdoc/>
public event EventHandler<bool>? UiHideToggled; public event EventHandler<bool>? UiHideToggled;
@ -137,33 +113,7 @@ internal sealed unsafe class GameGui : IInternalDisposableService, IGameGui
/// <inheritdoc/> /// <inheritdoc/>
public bool OpenMapWithMapLink(MapLinkPayload mapLink) public bool OpenMapWithMapLink(MapLinkPayload mapLink)
{ => RaptureAtkModule.Instance()->OpenMapWithMapLink(mapLink.DataString);
var uiModule = this.GetUIModule();
if (uiModule == IntPtr.Zero)
{
Log.Error("OpenMapWithMapLink: Null pointer returned from getUIObject()");
return false;
}
this.getUIMapObject ??= this.address.GetVirtualFunction<GetUIMapObjectDelegate>(uiModule, 0, 8);
var uiMapObjectPtr = this.getUIMapObject(uiModule);
if (uiMapObjectPtr == IntPtr.Zero)
{
Log.Error("OpenMapWithMapLink: Null pointer returned from GetUIMapObject()");
return false;
}
this.openMapWithFlag ??= this.address.GetVirtualFunction<OpenMapWithFlagDelegate>(uiMapObjectPtr, 0, 63);
var mapLinkString = mapLink.DataString;
Log.Debug($"OpenMapWithMapLink: Opening Map Link: {mapLinkString}");
return this.openMapWithFlag(uiMapObjectPtr, mapLinkString);
}
/// <inheritdoc/> /// <inheritdoc/>
public bool WorldToScreen(Vector3 worldPos, out Vector2 screenPos) public bool WorldToScreen(Vector3 worldPos, out Vector2 screenPos)
@ -311,11 +261,11 @@ internal sealed unsafe class GameGui : IInternalDisposableService, IGameGui
/// </summary> /// </summary>
void IInternalDisposableService.DisposeService() void IInternalDisposableService.DisposeService()
{ {
this.framework.Update -= this.FrameworkUpdate;
this.setGlobalBgmHook.Dispose(); this.setGlobalBgmHook.Dispose();
this.handleItemHoverHook.Dispose();
this.handleItemOutHook.Dispose();
this.handleImmHook.Dispose(); this.handleImmHook.Dispose();
this.toggleUiHideHook.Dispose(); this.setUiVisibilityHook.Dispose();
this.handleActionHoverHook.Dispose(); this.handleActionHoverHook.Dispose();
this.handleActionOutHook.Dispose(); this.handleActionOutHook.Dispose();
this.utf8StringFromSequenceHook.Dispose(); this.utf8StringFromSequenceHook.Dispose();
@ -359,51 +309,6 @@ internal sealed unsafe class GameGui : IInternalDisposableService, IGameGui
return retVal; return retVal;
} }
private IntPtr HandleItemHoverDetour(IntPtr hoverState, IntPtr a2, IntPtr a3, ulong a4)
{
var retVal = this.handleItemHoverHook.Original(hoverState, a2, a3, a4);
if (retVal.ToInt64() == 22)
{
var itemId = (ulong)Marshal.ReadInt32(hoverState, 0x138);
this.HoveredItem = itemId;
this.HoveredItemChanged?.InvokeSafely(this, itemId);
Log.Verbose($"HoverItemId:{itemId} this:{hoverState.ToInt64()}");
}
return retVal;
}
private IntPtr HandleItemOutDetour(IntPtr hoverState, IntPtr a2, IntPtr a3, ulong a4)
{
var retVal = this.handleItemOutHook.Original(hoverState, a2, a3, a4);
if (a3 != IntPtr.Zero && a4 == 1)
{
var a3Val = Marshal.ReadByte(a3, 0x8);
if (a3Val == 255)
{
this.HoveredItem = 0ul;
try
{
this.HoveredItemChanged?.Invoke(this, 0ul);
}
catch (Exception e)
{
Log.Error(e, "Could not dispatch HoveredItemChanged event.");
}
Log.Verbose("HoverItemId: 0");
}
}
return retVal;
}
private void HandleActionHoverDetour(IntPtr hoverState, HoverActionKind actionKind, uint actionId, int a4, byte a5) private void HandleActionHoverDetour(IntPtr hoverState, HoverActionKind actionKind, uint actionId, int a4, byte a5)
{ {
this.handleActionHoverHook.Original(hoverState, actionKind, actionId, a4, a5); this.handleActionHoverHook.Original(hoverState, actionKind, actionId, a4, a5);
@ -445,16 +350,14 @@ internal sealed unsafe class GameGui : IInternalDisposableService, IGameGui
return retVal; return retVal;
} }
private IntPtr ToggleUiHideDetour(IntPtr thisPtr, bool unknownByte) private unsafe void SetUiVisibilityDetour(RaptureAtkModule* thisPtr, bool uiVisible)
{ {
var result = this.toggleUiHideHook.Original(thisPtr, unknownByte); this.setUiVisibilityHook.Original(thisPtr, uiVisible);
this.GameUiHidden = !RaptureAtkModule.Instance()->IsUiVisible; this.GameUiHidden = !RaptureAtkModule.Instance()->IsUiVisible;
this.UiHideToggled?.InvokeSafely(this, this.GameUiHidden); this.UiHideToggled?.InvokeSafely(this, this.GameUiHidden);
Log.Debug("UiHide toggled: {0}", this.GameUiHidden); Log.Debug("GameUiHidden: {0}", this.GameUiHidden);
return result;
} }
private char HandleImmDetour(IntPtr framework, char a2, byte a3) private char HandleImmDetour(IntPtr framework, char a2, byte a3)
@ -477,6 +380,24 @@ internal sealed unsafe class GameGui : IInternalDisposableService, IGameGui
return thisPtr; // this function shouldn't need to return but the original asm moves this into rax before returning so be safe? return thisPtr; // this function shouldn't need to return but the original asm moves this into rax before returning so be safe?
} }
private unsafe void FrameworkUpdate(IFramework framework)
{
var agentItemDetail = AgentItemDetail.Instance();
if (agentItemDetail != null)
{
var itemId = agentItemDetail->ItemId;
if (this.HoveredItem != itemId)
{
Log.Verbose($"HoveredItem changed: {itemId}");
this.HoveredItem = itemId;
this.HoveredItemChanged?.InvokeSafely(this, itemId);
}
}
}
} }
/// <summary> /// <summary>

View file

@ -15,16 +15,6 @@ internal sealed class GameGuiAddressResolver : BaseAddressResolver
/// </summary> /// </summary>
public IntPtr SetGlobalBgm { get; private set; } public IntPtr SetGlobalBgm { get; private set; }
/// <summary>
/// Gets the address of the native HandleItemHover method.
/// </summary>
public IntPtr HandleItemHover { get; private set; }
/// <summary>
/// Gets the address of the native HandleItemOut method.
/// </summary>
public IntPtr HandleItemOut { get; private set; }
/// <summary> /// <summary>
/// Gets the address of the native HandleActionHover method. /// Gets the address of the native HandleActionHover method.
/// </summary> /// </summary>
@ -40,11 +30,6 @@ internal sealed class GameGuiAddressResolver : BaseAddressResolver
/// </summary> /// </summary>
public IntPtr HandleImm { get; private set; } public IntPtr HandleImm { get; private set; }
/// <summary>
/// Gets the address of the native ToggleUiHide method.
/// </summary>
public IntPtr ToggleUiHide { get; private set; }
/// <summary> /// <summary>
/// Gets the address of the native Utf8StringFromSequence method. /// Gets the address of the native Utf8StringFromSequence method.
/// </summary> /// </summary>
@ -54,13 +39,10 @@ internal sealed class GameGuiAddressResolver : BaseAddressResolver
protected override void Setup64Bit(ISigScanner sig) protected override void Setup64Bit(ISigScanner sig)
{ {
this.SetGlobalBgm = sig.ScanText("E8 ?? ?? ?? ?? 8B 2F"); this.SetGlobalBgm = sig.ScanText("E8 ?? ?? ?? ?? 8B 2F");
this.HandleItemHover = sig.ScanText("E8 ?? ?? ?? ?? 48 8B 6C 24 48 48 8B 74 24 50 4C 89 B7 08 01 00 00");
this.HandleItemOut = sig.ScanText("48 89 5C 24 ?? 57 48 83 EC 20 48 8B FA 48 8B D9 4D");
this.HandleActionHover = sig.ScanText("E8 ?? ?? ?? ?? E9 ?? ?? ?? ?? 83 F8 0F"); this.HandleActionHover = sig.ScanText("E8 ?? ?? ?? ?? E9 ?? ?? ?? ?? 83 F8 0F");
this.HandleActionOut = sig.ScanText("48 89 5C 24 ?? 57 48 83 EC 20 48 8B DA 48 8B F9 4D 85 C0 74 1F"); this.HandleActionOut = sig.ScanText("48 89 5C 24 ?? 57 48 83 EC 20 48 8B DA 48 8B F9 4D 85 C0 74 1F");
this.HandleImm = sig.ScanText("E8 ?? ?? ?? ?? 84 C0 75 10 48 83 FF 09"); this.HandleImm = sig.ScanText("E8 ?? ?? ?? ?? 84 C0 75 10 48 83 FF 09");
this.ToggleUiHide = sig.ScanText("48 89 5C 24 ?? 48 89 74 24 ?? 57 48 83 EC ?? 44 0F B6 81");
this.Utf8StringFromSequence = sig.ScanText("48 89 5C 24 ?? 48 89 74 24 ?? 57 48 83 EC 20 48 8D 41 22 66 C7 41 ?? ?? ?? 48 89 01 49 8B D8"); this.Utf8StringFromSequence = sig.ScanText("48 89 5C 24 ?? 48 89 74 24 ?? 57 48 83 EC 20 48 8D 41 22 66 C7 41 ?? ?? ?? 48 89 01 49 8B D8");
} }
} }

View file

@ -1,4 +1,4 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using Dalamud.Game.ClientState.Objects; using Dalamud.Game.ClientState.Objects;
@ -20,16 +20,6 @@ namespace Dalamud.Game.Gui.NamePlate;
[ServiceManager.EarlyLoadedService] [ServiceManager.EarlyLoadedService]
internal sealed class NamePlateGui : IInternalDisposableService, INamePlateGui internal sealed class NamePlateGui : IInternalDisposableService, INamePlateGui
{ {
/// <summary>
/// The index for the number array used by the NamePlate addon.
/// </summary>
public const int NumberArrayIndex = 5;
/// <summary>
/// The index for the string array used by the NamePlate addon.
/// </summary>
public const int StringArrayIndex = 4;
/// <summary> /// <summary>
/// The index for of the FullUpdate entry in the NamePlate number array. /// The index for of the FullUpdate entry in the NamePlate number array.
/// </summary> /// </summary>
@ -81,18 +71,11 @@ internal sealed class NamePlateGui : IInternalDisposableService, INamePlateGui
/// <inheritdoc/> /// <inheritdoc/>
public unsafe void RequestRedraw() public unsafe void RequestRedraw()
{ {
var addon = this.gameGui.GetAddonByName("NamePlate"); var addon = (AddonNamePlate*)this.gameGui.GetAddonByName("NamePlate");
if (addon != 0) if (addon != null)
{ {
var raptureAtkModule = RaptureAtkModule.Instance(); addon->DoFullUpdate = 1;
if (raptureAtkModule == null) AtkStage.Instance()->GetNumberArrayData(NumberArrayType.NamePlate)->SetValue(NumberArrayFullUpdateIndex, 1);
{
return;
}
((AddonNamePlate*)addon)->DoFullUpdate = 1;
var namePlateNumberArrayData = raptureAtkModule->AtkArrayDataHolder.NumberArrays[NumberArrayIndex];
namePlateNumberArrayData->SetValue(NumberArrayFullUpdateIndex, 1);
} }
} }

View file

@ -140,9 +140,9 @@ internal unsafe class NamePlateUpdateContext : INamePlateUpdateContext
public void ResetState(AtkUnitBase* addon, NumberArrayData** numberArrayData, StringArrayData** stringArrayData) public void ResetState(AtkUnitBase* addon, NumberArrayData** numberArrayData, StringArrayData** stringArrayData)
{ {
this.Addon = (AddonNamePlate*)addon; this.Addon = (AddonNamePlate*)addon;
this.NumberData = numberArrayData[NamePlateGui.NumberArrayIndex]; this.NumberData = AtkStage.Instance()->GetNumberArrayData(NumberArrayType.NamePlate);
this.NumberStruct = (AddonNamePlate.AddonNamePlateNumberArray*)this.NumberData->IntArray; this.NumberStruct = (AddonNamePlate.AddonNamePlateNumberArray*)this.NumberData->IntArray;
this.StringData = stringArrayData[NamePlateGui.StringArrayIndex]; this.StringData = AtkStage.Instance()->GetStringArrayData(StringArrayType.NamePlate);
this.HasParts = false; this.HasParts = false;
this.ActiveNamePlateCount = this.NumberStruct->ActiveNamePlateCount; this.ActiveNamePlateCount = this.NumberStruct->ActiveNamePlateCount;

View file

@ -5,8 +5,11 @@ using Dalamud.Game.Text.SeStringHandling;
using Dalamud.Hooking; using Dalamud.Hooking;
using Dalamud.IoC; using Dalamud.IoC;
using Dalamud.IoC.Internal; using Dalamud.IoC.Internal;
using Dalamud.Memory;
using Dalamud.Plugin.Services; using Dalamud.Plugin.Services;
using FFXIVClientStructs.FFXIV.Client.UI;
namespace Dalamud.Game.Gui.Toast; namespace Dalamud.Game.Gui.Toast;
/// <summary> /// <summary>
@ -17,8 +20,6 @@ internal sealed partial class ToastGui : IInternalDisposableService, IToastGui
{ {
private const uint QuestToastCheckmarkMagic = 60081; private const uint QuestToastCheckmarkMagic = 60081;
private readonly ToastGuiAddressResolver address;
private readonly Queue<(byte[] Message, ToastOptions Options)> normalQueue = new(); private readonly Queue<(byte[] Message, ToastOptions Options)> normalQueue = new();
private readonly Queue<(byte[] Message, QuestToastOptions Options)> questQueue = new(); private readonly Queue<(byte[] Message, QuestToastOptions Options)> questQueue = new();
private readonly Queue<byte[]> errorQueue = new(); private readonly Queue<byte[]> errorQueue = new();
@ -30,16 +31,12 @@ internal sealed partial class ToastGui : IInternalDisposableService, IToastGui
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="ToastGui"/> class. /// Initializes a new instance of the <see cref="ToastGui"/> class.
/// </summary> /// </summary>
/// <param name="sigScanner">Sig scanner to use.</param>
[ServiceManager.ServiceConstructor] [ServiceManager.ServiceConstructor]
private ToastGui(TargetSigScanner sigScanner) private unsafe ToastGui()
{ {
this.address = new ToastGuiAddressResolver(); this.showNormalToastHook = Hook<ShowNormalToastDelegate>.FromAddress((nint)UIModule.StaticVirtualTablePointer->ShowWideText, this.HandleNormalToastDetour);
this.address.Setup(sigScanner); this.showQuestToastHook = Hook<ShowQuestToastDelegate>.FromAddress((nint)UIModule.StaticVirtualTablePointer->ShowText, this.HandleQuestToastDetour);
this.showErrorToastHook = Hook<ShowErrorToastDelegate>.FromAddress((nint)UIModule.StaticVirtualTablePointer->ShowErrorText, this.HandleErrorToastDetour);
this.showNormalToastHook = Hook<ShowNormalToastDelegate>.FromAddress(this.address.ShowNormalToast, this.HandleNormalToastDetour);
this.showQuestToastHook = Hook<ShowQuestToastDelegate>.FromAddress(this.address.ShowQuestToast, this.HandleQuestToastDetour);
this.showErrorToastHook = Hook<ShowErrorToastDelegate>.FromAddress(this.address.ShowErrorToast, this.HandleErrorToastDetour);
this.showNormalToastHook.Enable(); this.showNormalToastHook.Enable();
this.showQuestToastHook.Enable(); this.showQuestToastHook.Enable();
@ -48,16 +45,16 @@ internal sealed partial class ToastGui : IInternalDisposableService, IToastGui
#region Marshal delegates #region Marshal delegates
private delegate IntPtr ShowNormalToastDelegate(IntPtr manager, IntPtr text, int layer, byte isTop, byte isFast, int logMessageId); private unsafe delegate void ShowNormalToastDelegate(UIModule* thisPtr, byte* text, int layer, byte isTop, byte isFast, uint logMessageId);
private delegate byte ShowQuestToastDelegate(IntPtr manager, int position, IntPtr text, uint iconOrCheck1, byte playSound, uint iconOrCheck2, byte alsoPlaySound); private unsafe delegate void ShowQuestToastDelegate(UIModule* thisPtr, int position, byte* text, uint iconOrCheck1, byte playSound, uint iconOrCheck2, byte alsoPlaySound);
private delegate byte ShowErrorToastDelegate(IntPtr manager, IntPtr text, byte respectsHidingMaybe); private unsafe delegate void ShowErrorToastDelegate(UIModule* thisPtr, byte* text, byte respectsHidingMaybe);
#endregion #endregion
#region Events #region Events
/// <inheritdoc/> /// <inheritdoc/>
public event IToastGui.OnNormalToastDelegate? Toast; public event IToastGui.OnNormalToastDelegate? Toast;
@ -102,32 +99,6 @@ internal sealed partial class ToastGui : IInternalDisposableService, IToastGui
this.ShowError(message); this.ShowError(message);
} }
} }
private static byte[] Terminate(byte[] source)
{
var terminated = new byte[source.Length + 1];
Array.Copy(source, 0, terminated, 0, source.Length);
terminated[^1] = 0;
return terminated;
}
private SeString ParseString(IntPtr text)
{
var bytes = new List<byte>();
unsafe
{
var ptr = (byte*)text;
while (*ptr != 0)
{
bytes.Add(*ptr);
ptr += 1;
}
}
// call events
return SeString.Parse(bytes.ToArray());
}
} }
/// <summary> /// <summary>
@ -149,36 +120,30 @@ internal sealed partial class ToastGui
this.normalQueue.Enqueue((message.Encode(), options)); this.normalQueue.Enqueue((message.Encode(), options));
} }
private void ShowNormal(byte[] bytes, ToastOptions? options = null) private unsafe void ShowNormal(byte[] bytes, ToastOptions? options = null)
{ {
options ??= new ToastOptions(); options ??= new ToastOptions();
var manager = Service<GameGui>.GetNullable()?.GetUIModule(); fixed (byte* ptr = bytes.NullTerminate())
if (manager == null)
return;
// terminate the string
var terminated = Terminate(bytes);
unsafe
{ {
fixed (byte* ptr = terminated) this.HandleNormalToastDetour(
{ UIModule.Instance(),
this.HandleNormalToastDetour(manager!.Value, (IntPtr)ptr, 5, (byte)options.Position, (byte)options.Speed, 0); ptr,
} 5,
(byte)options.Position,
(byte)options.Speed,
0);
} }
} }
private IntPtr HandleNormalToastDetour(IntPtr manager, IntPtr text, int layer, byte isTop, byte isFast, int logMessageId) private unsafe void HandleNormalToastDetour(UIModule* thisPtr, byte* text, int layer, byte isTop, byte isFast, uint logMessageId)
{ {
if (text == IntPtr.Zero) if (text == null)
{ return;
return IntPtr.Zero;
}
// call events // call events
var isHandled = false; var isHandled = false;
var str = this.ParseString(text); var str = MemoryHelper.ReadSeStringNullTerminated((nint)text);
var options = new ToastOptions var options = new ToastOptions
{ {
Position = (ToastPosition)isTop, Position = (ToastPosition)isTop,
@ -189,18 +154,17 @@ internal sealed partial class ToastGui
// do nothing if handled // do nothing if handled
if (isHandled) if (isHandled)
{ return;
return IntPtr.Zero;
}
var terminated = Terminate(str.Encode()); fixed (byte* ptr = str.EncodeWithNullTerminator())
unsafe
{ {
fixed (byte* message = terminated) this.showNormalToastHook.Original(
{ thisPtr,
return this.showNormalToastHook.Original(manager, (IntPtr)message, layer, (byte)options.Position, (byte)options.Speed, logMessageId); ptr,
} layer,
(byte)(options.Position == ToastPosition.Top ? 1 : 0),
(byte)(options.Speed == ToastSpeed.Fast ? 1 : 0),
logMessageId);
} }
} }
} }
@ -224,45 +188,33 @@ internal sealed partial class ToastGui
this.questQueue.Enqueue((message.Encode(), options)); this.questQueue.Enqueue((message.Encode(), options));
} }
private void ShowQuest(byte[] bytes, QuestToastOptions? options = null) private unsafe void ShowQuest(byte[] bytes, QuestToastOptions? options = null)
{ {
options ??= new QuestToastOptions(); options ??= new QuestToastOptions();
var manager = Service<GameGui>.GetNullable()?.GetUIModule();
if (manager == null)
return;
// terminate the string
var terminated = Terminate(bytes);
var (ioc1, ioc2) = this.DetermineParameterOrder(options); var (ioc1, ioc2) = this.DetermineParameterOrder(options);
unsafe fixed (byte* ptr = bytes.NullTerminate())
{ {
fixed (byte* ptr = terminated) this.HandleQuestToastDetour(
{ UIModule.Instance(),
this.HandleQuestToastDetour( (int)options.Position,
manager!.Value, ptr,
(int)options.Position, ioc1,
(IntPtr)ptr, (byte)(options.PlaySound ? 1 : 0),
ioc1, ioc2,
options.PlaySound ? (byte)1 : (byte)0, 0);
ioc2,
0);
}
} }
} }
private byte HandleQuestToastDetour(IntPtr manager, int position, IntPtr text, uint iconOrCheck1, byte playSound, uint iconOrCheck2, byte alsoPlaySound) private unsafe void HandleQuestToastDetour(UIModule* thisPtr, int position, byte* text, uint iconOrCheck1, byte playSound, uint iconOrCheck2, byte alsoPlaySound)
{ {
if (text == IntPtr.Zero) if (text == null)
{ return;
return 0;
}
// call events // call events
var isHandled = false; var isHandled = false;
var str = this.ParseString(text); var str = SeString.Parse(text);
var options = new QuestToastOptions var options = new QuestToastOptions
{ {
Position = (QuestToastPosition)position, Position = (QuestToastPosition)position,
@ -275,27 +227,20 @@ internal sealed partial class ToastGui
// do nothing if handled // do nothing if handled
if (isHandled) if (isHandled)
{ return;
return 0;
}
var terminated = Terminate(str.Encode());
var (ioc1, ioc2) = this.DetermineParameterOrder(options); var (ioc1, ioc2) = this.DetermineParameterOrder(options);
unsafe fixed (byte* ptr = str.EncodeWithNullTerminator())
{ {
fixed (byte* message = terminated) this.showQuestToastHook.Original(
{ UIModule.Instance(),
return this.showQuestToastHook.Original( (int)options.Position,
manager, ptr,
(int)options.Position, ioc1,
(IntPtr)message, (byte)(options.PlaySound ? 1 : 0),
ioc1, ioc2,
options.PlaySound ? (byte)1 : (byte)0, 0);
ioc2,
0);
}
} }
} }
@ -324,51 +269,32 @@ internal sealed partial class ToastGui
this.errorQueue.Enqueue(message.Encode()); this.errorQueue.Enqueue(message.Encode());
} }
private void ShowError(byte[] bytes) private unsafe void ShowError(byte[] bytes)
{ {
var manager = Service<GameGui>.GetNullable()?.GetUIModule(); fixed (byte* ptr = bytes.NullTerminate())
if (manager == null)
return;
// terminate the string
var terminated = Terminate(bytes);
unsafe
{ {
fixed (byte* ptr = terminated) this.HandleErrorToastDetour(UIModule.Instance(), ptr, 0);
{
this.HandleErrorToastDetour(manager!.Value, (IntPtr)ptr, 0);
}
} }
} }
private byte HandleErrorToastDetour(IntPtr manager, IntPtr text, byte respectsHidingMaybe) private unsafe void HandleErrorToastDetour(UIModule* thisPtr, byte* text, byte respectsHidingMaybe)
{ {
if (text == IntPtr.Zero) if (text == null)
{ return;
return 0;
}
// call events // call events
var isHandled = false; var isHandled = false;
var str = this.ParseString(text); var str = SeString.Parse(text);
this.ErrorToast?.Invoke(ref str, ref isHandled); this.ErrorToast?.Invoke(ref str, ref isHandled);
// do nothing if handled // do nothing if handled
if (isHandled) if (isHandled)
{ return;
return 0;
}
var terminated = Terminate(str.Encode()); fixed (byte* ptr = str.EncodeWithNullTerminator())
unsafe
{ {
fixed (byte* message = terminated) this.showErrorToastHook.Original(thisPtr, ptr, respectsHidingMaybe);
{
return this.showErrorToastHook.Original(manager, (IntPtr)message, respectsHidingMaybe);
}
} }
} }
} }

View file

@ -1,30 +0,0 @@
namespace Dalamud.Game.Gui.Toast;
/// <summary>
/// An address resolver for the <see cref="ToastGui"/> class.
/// </summary>
internal class ToastGuiAddressResolver : BaseAddressResolver
{
/// <summary>
/// Gets the address of the native ShowNormalToast method.
/// </summary>
public IntPtr ShowNormalToast { get; private set; }
/// <summary>
/// Gets the address of the native ShowQuestToast method.
/// </summary>
public IntPtr ShowQuestToast { get; private set; }
/// <summary>
/// Gets the address of the ShowErrorToast method.
/// </summary>
public IntPtr ShowErrorToast { get; private set; }
/// <inheritdoc/>
protected override void Setup64Bit(ISigScanner sig)
{
this.ShowNormalToast = sig.ScanText("48 89 5C 24 ?? 48 89 6C 24 ?? 48 89 74 24 ?? 57 48 83 EC 30 83 3D ?? ?? ?? ?? ??");
this.ShowQuestToast = sig.ScanText("48 89 5C 24 ?? 48 89 6C 24 ?? 48 89 74 24 ?? 48 89 7C 24 ?? 41 56 48 83 EC 40 83 3D ?? ?? ?? ?? ??");
this.ShowErrorToast = sig.ScanText("48 89 5C 24 ?? 48 89 6C 24 ?? 48 89 74 24 ?? 57 48 83 EC 20 83 3D ?? ?? ?? ?? ?? 41 0F B6 F0");
}
}

View file

@ -6,9 +6,11 @@ using Dalamud.Game.Text.SeStringHandling.Payloads;
using Dalamud.Hooking; using Dalamud.Hooking;
using Dalamud.Interface.Internal; using Dalamud.Interface.Internal;
using Dalamud.Interface.Windowing; using Dalamud.Interface.Windowing;
using Dalamud.Logging.Internal;
using FFXIVClientStructs.FFXIV.Client.UI;
using FFXIVClientStructs.FFXIV.Client.UI.Agent;
using FFXIVClientStructs.FFXIV.Component.GUI; using FFXIVClientStructs.FFXIV.Component.GUI;
using Serilog;
using ValueType = FFXIVClientStructs.FFXIV.Component.GUI.ValueType; using ValueType = FFXIVClientStructs.FFXIV.Component.GUI.ValueType;
@ -20,12 +22,14 @@ namespace Dalamud.Game.Internal;
[ServiceManager.EarlyLoadedService] [ServiceManager.EarlyLoadedService]
internal sealed unsafe class DalamudAtkTweaks : IInternalDisposableService internal sealed unsafe class DalamudAtkTweaks : IInternalDisposableService
{ {
private static readonly ModuleLog Log = new("DalamudAtkTweaks");
private readonly Hook<AgentHudOpenSystemMenuPrototype> hookAgentHudOpenSystemMenu; private readonly Hook<AgentHudOpenSystemMenuPrototype> hookAgentHudOpenSystemMenu;
// TODO: Make this into events in Framework.Gui // TODO: Make this into events in Framework.Gui
private readonly Hook<UiModuleRequestMainCommand> hookUiModuleRequestMainCommand; private readonly Hook<UIModule.Delegates.ExecuteMainCommand> hookUiModuleExecuteMainCommand;
private readonly Hook<AtkUnitBaseReceiveGlobalEvent> hookAtkUnitBaseReceiveGlobalEvent; private readonly Hook<AtkUnitBase.Delegates.ReceiveGlobalEvent> hookAtkUnitBaseReceiveGlobalEvent;
[ServiceManager.ServiceDependency] [ServiceManager.ServiceDependency]
private readonly DalamudConfiguration configuration = Service<DalamudConfiguration>.Get(); private readonly DalamudConfiguration configuration = Service<DalamudConfiguration>.Get();
@ -44,12 +48,8 @@ internal sealed unsafe class DalamudAtkTweaks : IInternalDisposableService
var openSystemMenuAddress = sigScanner.ScanText("E8 ?? ?? ?? ?? E9 ?? ?? ?? ?? 48 8B CF 4C 89 B4 24 B8 08 00 00"); var openSystemMenuAddress = sigScanner.ScanText("E8 ?? ?? ?? ?? E9 ?? ?? ?? ?? 48 8B CF 4C 89 B4 24 B8 08 00 00");
this.hookAgentHudOpenSystemMenu = Hook<AgentHudOpenSystemMenuPrototype>.FromAddress(openSystemMenuAddress, this.AgentHudOpenSystemMenuDetour); this.hookAgentHudOpenSystemMenu = Hook<AgentHudOpenSystemMenuPrototype>.FromAddress(openSystemMenuAddress, this.AgentHudOpenSystemMenuDetour);
this.hookUiModuleExecuteMainCommand = Hook<UIModule.Delegates.ExecuteMainCommand>.FromAddress((nint)UIModule.StaticVirtualTablePointer->ExecuteMainCommand, this.UiModuleExecuteMainCommandDetour);
var uiModuleRequestMainCommandAddress = sigScanner.ScanText("40 53 56 48 81 EC ?? ?? ?? ?? 48 8B 05 ?? ?? ?? ?? 48 33 C4 48 89 84 24 ?? ?? ?? ?? 48 8B 01 8B DA 48 8B F1 FF 90 ?? ?? ?? ??"); this.hookAtkUnitBaseReceiveGlobalEvent = Hook<AtkUnitBase.Delegates.ReceiveGlobalEvent>.FromAddress((nint)AtkUnitBase.StaticVirtualTablePointer->ReceiveGlobalEvent, this.AtkUnitBaseReceiveGlobalEventDetour);
this.hookUiModuleRequestMainCommand = Hook<UiModuleRequestMainCommand>.FromAddress(uiModuleRequestMainCommandAddress, this.UiModuleRequestMainCommandDetour);
var atkUnitBaseReceiveGlobalEventAddress = sigScanner.ScanText("48 89 5C 24 ?? 48 89 7C 24 ?? 55 41 54 41 57");
this.hookAtkUnitBaseReceiveGlobalEvent = Hook<AtkUnitBaseReceiveGlobalEvent>.FromAddress(atkUnitBaseReceiveGlobalEventAddress, this.AtkUnitBaseReceiveGlobalEventDetour);
this.locDalamudPlugins = Loc.Localize("SystemMenuPlugins", "Dalamud Plugins"); this.locDalamudPlugins = Loc.Localize("SystemMenuPlugins", "Dalamud Plugins");
this.locDalamudSettings = Loc.Localize("SystemMenuSettings", "Dalamud Settings"); this.locDalamudSettings = Loc.Localize("SystemMenuSettings", "Dalamud Settings");
@ -57,18 +57,14 @@ internal sealed unsafe class DalamudAtkTweaks : IInternalDisposableService
// this.contextMenu.ContextMenuOpened += this.ContextMenuOnContextMenuOpened; // this.contextMenu.ContextMenuOpened += this.ContextMenuOnContextMenuOpened;
this.hookAgentHudOpenSystemMenu.Enable(); this.hookAgentHudOpenSystemMenu.Enable();
this.hookUiModuleRequestMainCommand.Enable(); this.hookUiModuleExecuteMainCommand.Enable();
this.hookAtkUnitBaseReceiveGlobalEvent.Enable(); this.hookAtkUnitBaseReceiveGlobalEvent.Enable();
} }
/// <summary>Finalizes an instance of the <see cref="DalamudAtkTweaks"/> class.</summary> /// <summary>Finalizes an instance of the <see cref="DalamudAtkTweaks"/> class.</summary>
~DalamudAtkTweaks() => this.Dispose(false); ~DalamudAtkTweaks() => this.Dispose(false);
private delegate void AgentHudOpenSystemMenuPrototype(void* thisPtr, AtkValue* atkValueArgs, uint menuSize); private delegate void AgentHudOpenSystemMenuPrototype(AgentHUD* thisPtr, AtkValue* atkValueArgs, uint menuSize);
private delegate void UiModuleRequestMainCommand(void* thisPtr, int commandId);
private delegate IntPtr AtkUnitBaseReceiveGlobalEvent(AtkUnitBase* thisPtr, ushort cmd, uint a3, IntPtr a4, uint* a5);
/// <inheritdoc/> /// <inheritdoc/>
void IInternalDisposableService.DisposeService() => this.Dispose(true); void IInternalDisposableService.DisposeService() => this.Dispose(true);
@ -81,7 +77,7 @@ internal sealed unsafe class DalamudAtkTweaks : IInternalDisposableService
if (disposing) if (disposing)
{ {
this.hookAgentHudOpenSystemMenu.Dispose(); this.hookAgentHudOpenSystemMenu.Dispose();
this.hookUiModuleRequestMainCommand.Dispose(); this.hookUiModuleExecuteMainCommand.Dispose();
this.hookAtkUnitBaseReceiveGlobalEvent.Dispose(); this.hookAtkUnitBaseReceiveGlobalEvent.Dispose();
// this.contextMenu.ContextMenuOpened -= this.ContextMenuOnContextMenuOpened; // this.contextMenu.ContextMenuOpened -= this.ContextMenuOnContextMenuOpened;
@ -116,22 +112,19 @@ internal sealed unsafe class DalamudAtkTweaks : IInternalDisposableService
} }
*/ */
private IntPtr AtkUnitBaseReceiveGlobalEventDetour(AtkUnitBase* thisPtr, ushort cmd, uint a3, IntPtr a4, uint* arg) private void AtkUnitBaseReceiveGlobalEventDetour(AtkUnitBase* thisPtr, AtkEventType eventType, int eventParam, AtkEvent* atkEvent, AtkEventData* atkEventData)
{ {
// Log.Information("{0}: cmd#{1} a3#{2} - HasAnyFocus:{3}", MemoryHelper.ReadSeStringAsString(out _, new IntPtr(thisPtr->Name)), cmd, a3, WindowSystem.HasAnyWindowSystemFocus);
// "SendHotkey"
// 3 == Close // 3 == Close
if (cmd == 12 && WindowSystem.HasAnyWindowSystemFocus && *arg == 3 && this.configuration.IsFocusManagementEnabled) if (eventType == AtkEventType.InputReceived && WindowSystem.HasAnyWindowSystemFocus && atkEventData != null && *(int*)atkEventData == 3 && this.configuration.IsFocusManagementEnabled)
{ {
Log.Verbose($"Cancelling global event SendHotkey command due to WindowSystem {WindowSystem.FocusedWindowSystemNamespace}"); Log.Verbose($"Cancelling global event SendHotkey command due to WindowSystem {WindowSystem.FocusedWindowSystemNamespace}");
return IntPtr.Zero; return;
} }
return this.hookAtkUnitBaseReceiveGlobalEvent.Original(thisPtr, cmd, a3, a4, arg); this.hookAtkUnitBaseReceiveGlobalEvent.Original(thisPtr, eventType, eventParam, atkEvent, atkEventData);
} }
private void AgentHudOpenSystemMenuDetour(void* thisPtr, AtkValue* atkValueArgs, uint menuSize) private void AgentHudOpenSystemMenuDetour(AgentHUD* thisPtr, AtkValue* atkValueArgs, uint menuSize)
{ {
if (WindowSystem.HasAnyWindowSystemFocus && this.configuration.IsFocusManagementEnabled) if (WindowSystem.HasAnyWindowSystemFocus && this.configuration.IsFocusManagementEnabled)
{ {
@ -213,7 +206,7 @@ internal sealed unsafe class DalamudAtkTweaks : IInternalDisposableService
this.hookAgentHudOpenSystemMenu.Original(thisPtr, atkValueArgs, menuSize + 2); this.hookAgentHudOpenSystemMenu.Original(thisPtr, atkValueArgs, menuSize + 2);
} }
private void UiModuleRequestMainCommandDetour(void* thisPtr, int commandId) private unsafe void UiModuleExecuteMainCommandDetour(UIModule* thisPtr, uint commandId)
{ {
var dalamudInterface = Service<DalamudInterface>.GetNullable(); var dalamudInterface = Service<DalamudInterface>.GetNullable();
@ -226,7 +219,7 @@ internal sealed unsafe class DalamudAtkTweaks : IInternalDisposableService
dalamudInterface?.OpenSettings(); dalamudInterface?.OpenSettings();
break; break;
default: default:
this.hookUiModuleRequestMainCommand.Original(thisPtr, commandId); this.hookUiModuleExecuteMainCommand.Original(thisPtr, commandId);
break; break;
} }
} }

View file

@ -1,4 +1,4 @@
using Dalamud.Game.ClientState.Buddy; using Dalamud.Game.ClientState.Buddy;
using Dalamud.Utility; using Dalamud.Utility;
using ImGuiNET; using ImGuiNET;
@ -32,8 +32,6 @@ internal class BuddyListWidget : IDataWindowWidget
var buddyList = Service<BuddyList>.Get(); var buddyList = Service<BuddyList>.Get();
ImGui.Checkbox("Resolve GameData", ref this.resolveGameData); ImGui.Checkbox("Resolve GameData", ref this.resolveGameData);
ImGui.Text($"BuddyList: {buddyList.BuddyListAddress.ToInt64():X}");
{ {
var member = buddyList.CompanionBuddy; var member = buddyList.CompanionBuddy;
if (member == null) if (member == null)

View file

@ -72,7 +72,7 @@ public interface IChatGui
/// <summary> /// <summary>
/// Gets the ID of the last linked item. /// Gets the ID of the last linked item.
/// </summary> /// </summary>
public int LastLinkedItemId { get; } public uint LastLinkedItemId { get; }
/// <summary> /// <summary>
/// Gets the flags of the last linked item. /// Gets the flags of the last linked item.