From c232befd83d0f2656b417ab7a89a148252d956b9 Mon Sep 17 00:00:00 2001 From: goaaats <16760685+goaaats@users.noreply.github.com> Date: Thu, 27 Jan 2022 01:04:46 +0100 Subject: [PATCH] fix(ItemPayload): thorough clean up, collectibles, eventitems, aging --- Dalamud/Game/Gui/GameGui.cs | 21 ++- Dalamud/Game/Gui/GameGuiAddressResolver.cs | 6 + Dalamud/Game/Text/SeIconChar.cs | 5 + .../SeStringHandling/Payloads/ItemPayload.cs | 54 +++++--- .../SeStringHandling/Payloads/TextPayload.cs | 11 +- .../Game/Text/SeStringHandling/SeString.cs | 44 ++++++- .../Text/SeStringHandling/SeStringBuilder.cs | 8 ++ .../AgingSteps/ItemPayloadAgingStep.cs | 120 ++++++++++++++++++ .../Windows/SelfTest/SelfTestWindow.cs | 2 + 9 files changed, 244 insertions(+), 27 deletions(-) create mode 100644 Dalamud/Interface/Internal/Windows/SelfTest/AgingSteps/ItemPayloadAgingStep.cs diff --git a/Dalamud/Game/Gui/GameGui.cs b/Dalamud/Game/Gui/GameGui.cs index fce32155c..272b0e1f8 100644 --- a/Dalamud/Game/Gui/GameGui.cs +++ b/Dalamud/Game/Gui/GameGui.cs @@ -11,6 +11,7 @@ using Dalamud.Interface; using Dalamud.IoC; using Dalamud.IoC.Internal; using Dalamud.Utility; +using FFXIVClientStructs.FFXIV.Client.System.String; using ImGuiNET; using Serilog; @@ -21,7 +22,7 @@ namespace Dalamud.Game.Gui /// [PluginInterface] [InterfaceVersion("1.0")] - public sealed class GameGui : IDisposable + public sealed unsafe class GameGui : IDisposable { private readonly GameGuiAddressResolver address; @@ -36,6 +37,7 @@ namespace Dalamud.Game.Gui private readonly Hook handleActionOutHook; private readonly Hook handleImmHook; private readonly Hook toggleUiHideHook; + private readonly Hook utf8StringFromSequenceHook; private GetUIMapObjectDelegate getUIMapObject; private OpenMapWithFlagDelegate openMapWithFlag; @@ -79,6 +81,8 @@ namespace Dalamud.Game.Gui this.toggleUiHideHook = new Hook(this.address.ToggleUiHide, this.ToggleUiHideDetour); this.getAgentModule = Marshal.GetDelegateForFunctionPointer(this.address.GetAgentModule); + + this.utf8StringFromSequenceHook = new Hook(this.address.Utf8StringFromSequence, this.Utf8StringFromSequenceDetour); } // Marshaled delegates @@ -93,6 +97,9 @@ namespace Dalamud.Game.Gui // Hooked delegates + [UnmanagedFunctionPointer(CallingConvention.ThisCall)] + private delegate Utf8String* Utf8StringFromSequenceDelegate(Utf8String* thisPtr, byte* sourcePtr, nuint sourceLen); + [UnmanagedFunctionPointer(CallingConvention.ThisCall)] private delegate IntPtr GetUIMapObjectDelegate(IntPtr uiObject); @@ -439,6 +446,7 @@ namespace Dalamud.Game.Gui this.toggleUiHideHook.Enable(); this.handleActionHoverHook.Enable(); this.handleActionOutHook.Enable(); + this.utf8StringFromSequenceHook.Enable(); } /// @@ -457,6 +465,7 @@ namespace Dalamud.Game.Gui this.toggleUiHideHook.Dispose(); this.handleActionHoverHook.Dispose(); this.handleActionOutHook.Dispose(); + this.utf8StringFromSequenceHook.Dispose(); } /// @@ -602,5 +611,15 @@ namespace Dalamud.Game.Gui ? (char)0 : result; } + + private Utf8String* Utf8StringFromSequenceDetour(Utf8String* thisPtr, byte* sourcePtr, nuint sourceLen) + { + if (sourcePtr != null) + this.utf8StringFromSequenceHook.Original(thisPtr, sourcePtr, sourceLen); + else + thisPtr->Ctor(); // this is in clientstructs but you could do it manually too + + return thisPtr; // this function shouldn't need to return but the original asm moves this into rax before returning so be safe? + } } } diff --git a/Dalamud/Game/Gui/GameGuiAddressResolver.cs b/Dalamud/Game/Gui/GameGuiAddressResolver.cs index 122a9eea2..adeaab1af 100644 --- a/Dalamud/Game/Gui/GameGuiAddressResolver.cs +++ b/Dalamud/Game/Gui/GameGuiAddressResolver.cs @@ -76,6 +76,11 @@ namespace Dalamud.Game.Gui /// public IntPtr GetAgentModule { get; private set; } + /// + /// Gets the address of the native Utf8StringFromSequence method. + /// + public IntPtr Utf8StringFromSequence { get; private set; } + /// protected override void Setup64Bit(SigScanner sig) { @@ -88,6 +93,7 @@ namespace Dalamud.Game.Gui this.GetMatrixSingleton = sig.ScanText("E8 ?? ?? ?? ?? 48 8D 4C 24 ?? 48 89 4c 24 ?? 4C 8D 4D ?? 4C 8D 44 24 ??"); this.ScreenToWorld = sig.ScanText("48 83 EC 48 48 8B 05 ?? ?? ?? ?? 4D 8B D1"); this.ToggleUiHide = sig.ScanText("48 89 5C 24 ?? 48 89 74 24 ?? 57 48 83 EC 20 0F B6 B9 ?? ?? ?? ?? B8 ?? ?? ?? ??"); + 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"); var uiModuleVtableSig = sig.GetStaticAddressFromSig("48 8D 05 ?? ?? ?? ?? 4C 89 61 28"); this.GetAgentModule = Marshal.ReadIntPtr(uiModuleVtableSig, 34 * IntPtr.Size); diff --git a/Dalamud/Game/Text/SeIconChar.cs b/Dalamud/Game/Text/SeIconChar.cs index 539868135..b913b2ec9 100644 --- a/Dalamud/Game/Text/SeIconChar.cs +++ b/Dalamud/Game/Text/SeIconChar.cs @@ -30,6 +30,11 @@ namespace Dalamud.Game.Text /// HighQuality = 0xE03C, + /// + /// The collectible icon unicode character. + /// + Collectible = 0xE03D, + /// /// The clock icon unicode character. /// diff --git a/Dalamud/Game/Text/SeStringHandling/Payloads/ItemPayload.cs b/Dalamud/Game/Text/SeStringHandling/Payloads/ItemPayload.cs index df6831354..8ab3d2484 100644 --- a/Dalamud/Game/Text/SeStringHandling/Payloads/ItemPayload.cs +++ b/Dalamud/Game/Text/SeStringHandling/Payloads/ItemPayload.cs @@ -1,3 +1,4 @@ +using System; using System.Collections.Generic; using System.IO; using System.Linq; @@ -5,6 +6,7 @@ using System.Text; using Lumina.Excel.GeneratedSheets; using Newtonsoft.Json; +using Serilog; namespace Dalamud.Game.Text.SeStringHandling.Payloads { @@ -22,7 +24,7 @@ namespace Dalamud.Game.Text.SeStringHandling.Payloads private string? displayName; [JsonProperty] - private uint itemId; + private uint rawItemId; /// /// Initializes a new instance of the class. @@ -35,7 +37,10 @@ namespace Dalamud.Game.Text.SeStringHandling.Payloads /// TextPayload that is a part of a full item link in chat. public ItemPayload(uint itemId, bool isHq, string? displayNameOverride = null) { - this.itemId = itemId; + this.rawItemId = itemId; + if (isHq) + this.rawItemId += (uint)ItemKind.Hq; + this.Kind = isHq ? ItemKind.Hq : ItemKind.Normal; this.displayName = displayNameOverride; } @@ -51,8 +56,11 @@ namespace Dalamud.Game.Text.SeStringHandling.Payloads /// TextPayload that is a part of a full item link in chat. public ItemPayload(uint itemId, ItemKind kind = ItemKind.Normal, string? displayNameOverride = null) { - this.itemId = itemId; this.Kind = kind; + this.rawItemId = itemId; + if (kind != ItemKind.EventItem) + this.rawItemId += (uint)kind; + this.displayName = displayNameOverride; } @@ -112,10 +120,16 @@ namespace Dalamud.Game.Text.SeStringHandling.Payloads } /// - /// Gets the raw item ID of this payload. + /// Gets the actual item ID of this payload. /// [JsonIgnore] - public uint ItemId => this.itemId; + public uint ItemId => GetAdjustedId(this.rawItemId).ItemId; + + /// + /// Gets the raw, unadjusted item ID of this payload. + /// + [JsonIgnore] + public uint RawItemId => this.rawItemId; /// /// Gets the underlying Lumina Item represented by this payload. @@ -124,7 +138,21 @@ namespace Dalamud.Game.Text.SeStringHandling.Payloads /// The value is evaluated lazily and cached. /// [JsonIgnore] - public Item? Item => this.item ??= this.DataResolver.GetExcelSheet()!.GetRow(this.itemId); + public Item? Item + { + get + { + // TODO(goat): This should be revamped/removed on an API level change. + if (this.Kind == ItemKind.EventItem) + { + Log.Warning("Event items cannot be fetched from the ItemPayload"); + return null; + } + + this.item ??= this.DataResolver.GetExcelSheet()!.GetRow(this.ItemId); + return this.item; + } + } /// /// Gets a value indicating whether or not this item link is for a high-quality version of the item. @@ -156,15 +184,13 @@ namespace Dalamud.Game.Text.SeStringHandling.Payloads /// public override string ToString() { - return $"{this.Type} - ItemId: {this.itemId}, Kind: {this.Kind}, Name: {this.displayName ?? this.Item?.Name}"; + return $"{this.Type} - ItemId: {this.ItemId}, Kind: {this.Kind}, Name: {this.displayName ?? this.Item?.Name}"; } /// protected override byte[] EncodeImpl() { - var actualItemId = this.itemId - (uint)this.Kind; - - var idBytes = MakeInteger(actualItemId); + var idBytes = MakeInteger(this.rawItemId); var hasName = !string.IsNullOrEmpty(this.displayName); var chunkLen = idBytes.Length + 4; @@ -218,10 +244,8 @@ namespace Dalamud.Game.Text.SeStringHandling.Payloads /// protected override void DecodeImpl(BinaryReader reader, long endOfStream) { - var (id, kind) = GetAdjustedId(GetInteger(reader)); - - this.itemId = id; - this.Kind = kind; + this.rawItemId = GetInteger(reader); + this.Kind = GetAdjustedId(this.rawItemId).Kind; if (reader.BaseStream.Position + 3 < endOfStream) { @@ -253,7 +277,7 @@ namespace Dalamud.Game.Text.SeStringHandling.Payloads { > 500_000 and < 1_000_000 => (rawItemId - 500_000, ItemKind.Collectible), > 1_000_000 and < 2_000_000 => (rawItemId - 1_000_000, ItemKind.Hq), - > 2_000_000 => (rawItemId - 2_000_000, ItemKind.EventItem), + > 2_000_000 => (rawItemId, ItemKind.EventItem), // EventItem IDs are NOT adjusted _ => (rawItemId, ItemKind.Normal), }; } diff --git a/Dalamud/Game/Text/SeStringHandling/Payloads/TextPayload.cs b/Dalamud/Game/Text/SeStringHandling/Payloads/TextPayload.cs index d12bdd3a7..8242f8b3f 100644 --- a/Dalamud/Game/Text/SeStringHandling/Payloads/TextPayload.cs +++ b/Dalamud/Game/Text/SeStringHandling/Payloads/TextPayload.cs @@ -13,14 +13,14 @@ namespace Dalamud.Game.Text.SeStringHandling.Payloads public class TextPayload : Payload, ITextProvider { [JsonProperty] - private string text; + private string? text; /// /// Initializes a new instance of the class. /// Creates a new TextPayload for the given text. /// /// The text to include for this payload. - public TextPayload(string text) + public TextPayload(string? text) { this.text = text; } @@ -41,12 +41,9 @@ namespace Dalamud.Game.Text.SeStringHandling.Payloads /// This may contain SE's special unicode characters. /// [JsonIgnore] - public string Text + public string? Text { - get - { - return this.text; - } + get => this.text; set { diff --git a/Dalamud/Game/Text/SeStringHandling/SeString.cs b/Dalamud/Game/Text/SeStringHandling/SeString.cs index c7c3cff34..012839c00 100644 --- a/Dalamud/Game/Text/SeStringHandling/SeString.cs +++ b/Dalamud/Game/Text/SeStringHandling/SeString.cs @@ -156,22 +156,58 @@ namespace Dalamud.Game.Text.SeStringHandling /// Whether to link the high-quality variant of the item. /// An optional name override to display, instead of the actual item name. /// An SeString containing all the payloads necessary to display an item link in the chat log. - public static SeString CreateItemLink(uint itemId, bool isHq, string? displayNameOverride = null) + public static SeString CreateItemLink(uint itemId, bool isHq, string? displayNameOverride = null) => + CreateItemLink(itemId, isHq ? ItemPayload.ItemKind.Hq : ItemPayload.ItemKind.Normal, displayNameOverride); + + /// + /// Creates an SeString representing an entire Payload chain that can be used to link an item in the chat log. + /// + /// The id of the item to link. + /// The kind of item to link. + /// An optional name override to display, instead of the actual item name. + /// An SeString containing all the payloads necessary to display an item link in the chat log. + public static SeString CreateItemLink(uint itemId, ItemPayload.ItemKind kind = ItemPayload.ItemKind.Normal, string? displayNameOverride = null) { var data = Service.Get(); - var displayName = displayNameOverride ?? data.GetExcelSheet()?.GetRow(itemId)?.Name; - if (isHq) + var displayName = displayNameOverride; + if (displayName == null) + { + switch (kind) + { + case ItemPayload.ItemKind.Normal: + case ItemPayload.ItemKind.Collectible: + case ItemPayload.ItemKind.Hq: + displayName = data.GetExcelSheet()?.GetRow(itemId)?.Name; + break; + case ItemPayload.ItemKind.EventItem: + displayName = data.GetExcelSheet()?.GetRow(itemId)?.Name; + break; + default: + throw new ArgumentOutOfRangeException(nameof(kind), kind, null); + } + } + + if (displayName == null) + { + throw new Exception("Invalid item ID specified, could not determine item name."); + } + + if (kind == ItemPayload.ItemKind.Hq) { displayName += $" {(char)SeIconChar.HighQuality}"; } + else if (kind == ItemPayload.ItemKind.Collectible) + { + displayName += $" {(char)SeIconChar.Collectible}"; + } // TODO: probably a cleaner way to build these than doing the bulk+insert var payloads = new List(new Payload[] { new UIForegroundPayload(0x0225), new UIGlowPayload(0x0226), - new ItemPayload(itemId, isHq), + new ItemPayload(itemId, kind), // arrow goes here new TextPayload(displayName), RawPayload.LinkTerminator, diff --git a/Dalamud/Game/Text/SeStringHandling/SeStringBuilder.cs b/Dalamud/Game/Text/SeStringHandling/SeStringBuilder.cs index 36438c75c..a1401594d 100644 --- a/Dalamud/Game/Text/SeStringHandling/SeStringBuilder.cs +++ b/Dalamud/Game/Text/SeStringHandling/SeStringBuilder.cs @@ -116,6 +116,14 @@ namespace Dalamud.Game.Text.SeStringHandling public SeStringBuilder AddItemLink(uint itemId, ItemPayload.ItemKind kind, string? itemNameOverride = null) => this.Add(new ItemPayload(itemId, kind, itemNameOverride)); + /// + /// Add an item link to the builder. + /// + /// The raw item ID. + /// The current builder. + public SeStringBuilder AddItemLinkRaw(uint rawItemId) => + this.Add(ItemPayload.FromRaw(rawItemId)); + /// /// Add italicized raw text to the builder. /// diff --git a/Dalamud/Interface/Internal/Windows/SelfTest/AgingSteps/ItemPayloadAgingStep.cs b/Dalamud/Interface/Internal/Windows/SelfTest/AgingSteps/ItemPayloadAgingStep.cs new file mode 100644 index 000000000..e45e88715 --- /dev/null +++ b/Dalamud/Interface/Internal/Windows/SelfTest/AgingSteps/ItemPayloadAgingStep.cs @@ -0,0 +1,120 @@ +using System; + +using Dalamud.Game.Gui; +using Dalamud.Game.Text.SeStringHandling; +using Dalamud.Game.Text.SeStringHandling.Payloads; +using ImGuiNET; + +namespace Dalamud.Interface.Internal.Windows.SelfTest.AgingSteps +{ + /// + /// Test setup for item payloads. + /// + internal class ItemPayloadAgingStep : IAgingStep + { + private SubStep currentSubStep; + + private enum SubStep + { + PrintNormalItem, + HoverNormalItem, + PrintHqItem, + HoverHqItem, + PrintCollectable, + HoverCollectable, + PrintEventItem, + HoverEventItem, + PrintNormalWithText, + HoverNormalWithText, + Done, + } + + /// + public string Name => "Item Payloads"; + + /// + public SelfTestStepResult RunStep() + { + var gameGui = Service.Get(); + var chatGui = Service.Get(); + + const uint normalItemId = 24002; // Capybara pup + const uint hqItemId = 31861; // Exarchic circlets of healing + const uint collectableItemId = 36299; // Rarefied Annite + const uint eventItemId = 2003363; // Speude bradeos figurine + + SeString? toPrint = null; + + ImGui.Text(this.currentSubStep.ToString()); + + switch (this.currentSubStep) + { + case SubStep.PrintNormalItem: + toPrint = SeString.CreateItemLink(normalItemId); + this.currentSubStep++; + break; + case SubStep.HoverNormalItem: + ImGui.Text("Hover the item."); + if (gameGui.HoveredItem != normalItemId) + return SelfTestStepResult.Waiting; + this.currentSubStep++; + break; + case SubStep.PrintHqItem: + toPrint = SeString.CreateItemLink(hqItemId, ItemPayload.ItemKind.Hq); + this.currentSubStep++; + break; + case SubStep.HoverHqItem: + ImGui.Text("Hover the item."); + if (gameGui.HoveredItem != 1_000_000 + hqItemId) + return SelfTestStepResult.Waiting; + this.currentSubStep++; + break; + case SubStep.PrintCollectable: + toPrint = SeString.CreateItemLink(collectableItemId, ItemPayload.ItemKind.Collectible); + this.currentSubStep++; + break; + case SubStep.HoverCollectable: + ImGui.Text("Hover the item."); + if (gameGui.HoveredItem != 500_000 + collectableItemId) + return SelfTestStepResult.Waiting; + this.currentSubStep++; + break; + case SubStep.PrintEventItem: + toPrint = SeString.CreateItemLink(eventItemId, ItemPayload.ItemKind.EventItem); + this.currentSubStep++; + break; + case SubStep.HoverEventItem: + ImGui.Text("Hover the item."); + if (gameGui.HoveredItem != eventItemId) + return SelfTestStepResult.Waiting; + this.currentSubStep++; + break; + case SubStep.PrintNormalWithText: + toPrint = SeString.CreateItemLink(normalItemId, displayNameOverride: "Gort"); + this.currentSubStep++; + break; + case SubStep.HoverNormalWithText: + ImGui.Text("Hover the item."); + if (gameGui.HoveredItem != normalItemId) + return SelfTestStepResult.Waiting; + this.currentSubStep++; + break; + case SubStep.Done: + return SelfTestStepResult.Pass; + default: + throw new ArgumentOutOfRangeException(); + } + + if (toPrint != null) + chatGui.Print(toPrint); + + return SelfTestStepResult.Waiting; + } + + /// + public void CleanUp() + { + this.currentSubStep = SubStep.PrintNormalItem; + } + } +} diff --git a/Dalamud/Interface/Internal/Windows/SelfTest/SelfTestWindow.cs b/Dalamud/Interface/Internal/Windows/SelfTest/SelfTestWindow.cs index 4fd2762fd..09dfd920e 100644 --- a/Dalamud/Interface/Internal/Windows/SelfTest/SelfTestWindow.cs +++ b/Dalamud/Interface/Internal/Windows/SelfTest/SelfTestWindow.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Diagnostics; using System.Linq; using System.Numerics; @@ -26,6 +27,7 @@ namespace Dalamud.Interface.Internal.Windows.SelfTest new LoginEventAgingStep(), new WaitFramesAgingStep(1000), new EnterTerritoryAgingStep(148, "Central Shroud"), + new ItemPayloadAgingStep(), new ActorTableAgingStep(), new FateTableAgingStep(), new AetheryteListAgingStep(),