fix(ItemPayload): thorough clean up, collectibles, eventitems, aging

This commit is contained in:
goaaats 2022-01-27 01:04:46 +01:00
parent bb7128e6fe
commit c232befd83
No known key found for this signature in database
GPG key ID: F18F057873895461
9 changed files with 244 additions and 27 deletions

View file

@ -11,6 +11,7 @@ using Dalamud.Interface;
using Dalamud.IoC; using Dalamud.IoC;
using Dalamud.IoC.Internal; using Dalamud.IoC.Internal;
using Dalamud.Utility; using Dalamud.Utility;
using FFXIVClientStructs.FFXIV.Client.System.String;
using ImGuiNET; using ImGuiNET;
using Serilog; using Serilog;
@ -21,7 +22,7 @@ namespace Dalamud.Game.Gui
/// </summary> /// </summary>
[PluginInterface] [PluginInterface]
[InterfaceVersion("1.0")] [InterfaceVersion("1.0")]
public sealed class GameGui : IDisposable public sealed unsafe class GameGui : IDisposable
{ {
private readonly GameGuiAddressResolver address; private readonly GameGuiAddressResolver address;
@ -36,6 +37,7 @@ namespace Dalamud.Game.Gui
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<ToggleUiHideDelegate> toggleUiHideHook;
private readonly Hook<Utf8StringFromSequenceDelegate> utf8StringFromSequenceHook;
private GetUIMapObjectDelegate getUIMapObject; private GetUIMapObjectDelegate getUIMapObject;
private OpenMapWithFlagDelegate openMapWithFlag; private OpenMapWithFlagDelegate openMapWithFlag;
@ -79,6 +81,8 @@ namespace Dalamud.Game.Gui
this.toggleUiHideHook = new Hook<ToggleUiHideDelegate>(this.address.ToggleUiHide, this.ToggleUiHideDetour); this.toggleUiHideHook = new Hook<ToggleUiHideDelegate>(this.address.ToggleUiHide, this.ToggleUiHideDetour);
this.getAgentModule = Marshal.GetDelegateForFunctionPointer<GetAgentModuleDelegate>(this.address.GetAgentModule); this.getAgentModule = Marshal.GetDelegateForFunctionPointer<GetAgentModuleDelegate>(this.address.GetAgentModule);
this.utf8StringFromSequenceHook = new Hook<Utf8StringFromSequenceDelegate>(this.address.Utf8StringFromSequence, this.Utf8StringFromSequenceDetour);
} }
// Marshaled delegates // Marshaled delegates
@ -93,6 +97,9 @@ namespace Dalamud.Game.Gui
// Hooked delegates // Hooked delegates
[UnmanagedFunctionPointer(CallingConvention.ThisCall)]
private delegate Utf8String* Utf8StringFromSequenceDelegate(Utf8String* thisPtr, byte* sourcePtr, nuint sourceLen);
[UnmanagedFunctionPointer(CallingConvention.ThisCall)] [UnmanagedFunctionPointer(CallingConvention.ThisCall)]
private delegate IntPtr GetUIMapObjectDelegate(IntPtr uiObject); private delegate IntPtr GetUIMapObjectDelegate(IntPtr uiObject);
@ -439,6 +446,7 @@ namespace Dalamud.Game.Gui
this.toggleUiHideHook.Enable(); this.toggleUiHideHook.Enable();
this.handleActionHoverHook.Enable(); this.handleActionHoverHook.Enable();
this.handleActionOutHook.Enable(); this.handleActionOutHook.Enable();
this.utf8StringFromSequenceHook.Enable();
} }
/// <summary> /// <summary>
@ -457,6 +465,7 @@ namespace Dalamud.Game.Gui
this.toggleUiHideHook.Dispose(); this.toggleUiHideHook.Dispose();
this.handleActionHoverHook.Dispose(); this.handleActionHoverHook.Dispose();
this.handleActionOutHook.Dispose(); this.handleActionOutHook.Dispose();
this.utf8StringFromSequenceHook.Dispose();
} }
/// <summary> /// <summary>
@ -602,5 +611,15 @@ namespace Dalamud.Game.Gui
? (char)0 ? (char)0
: result; : 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?
}
} }
} }

View file

@ -76,6 +76,11 @@ namespace Dalamud.Game.Gui
/// </summary> /// </summary>
public IntPtr GetAgentModule { get; private set; } public IntPtr GetAgentModule { get; private set; }
/// <summary>
/// Gets the address of the native Utf8StringFromSequence method.
/// </summary>
public IntPtr Utf8StringFromSequence { get; private set; }
/// <inheritdoc/> /// <inheritdoc/>
protected override void Setup64Bit(SigScanner sig) 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.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.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.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"); var uiModuleVtableSig = sig.GetStaticAddressFromSig("48 8D 05 ?? ?? ?? ?? 4C 89 61 28");
this.GetAgentModule = Marshal.ReadIntPtr(uiModuleVtableSig, 34 * IntPtr.Size); this.GetAgentModule = Marshal.ReadIntPtr(uiModuleVtableSig, 34 * IntPtr.Size);

View file

@ -30,6 +30,11 @@ namespace Dalamud.Game.Text
/// </summary> /// </summary>
HighQuality = 0xE03C, HighQuality = 0xE03C,
/// <summary>
/// The collectible icon unicode character.
/// </summary>
Collectible = 0xE03D,
/// <summary> /// <summary>
/// The clock icon unicode character. /// The clock icon unicode character.
/// </summary> /// </summary>

View file

@ -1,3 +1,4 @@
using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
@ -5,6 +6,7 @@ using System.Text;
using Lumina.Excel.GeneratedSheets; using Lumina.Excel.GeneratedSheets;
using Newtonsoft.Json; using Newtonsoft.Json;
using Serilog;
namespace Dalamud.Game.Text.SeStringHandling.Payloads namespace Dalamud.Game.Text.SeStringHandling.Payloads
{ {
@ -22,7 +24,7 @@ namespace Dalamud.Game.Text.SeStringHandling.Payloads
private string? displayName; private string? displayName;
[JsonProperty] [JsonProperty]
private uint itemId; private uint rawItemId;
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="ItemPayload"/> class. /// Initializes a new instance of the <see cref="ItemPayload"/> class.
@ -35,7 +37,10 @@ namespace Dalamud.Game.Text.SeStringHandling.Payloads
/// TextPayload that is a part of a full item link in chat.</param> /// TextPayload that is a part of a full item link in chat.</param>
public ItemPayload(uint itemId, bool isHq, string? displayNameOverride = null) 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.Kind = isHq ? ItemKind.Hq : ItemKind.Normal;
this.displayName = displayNameOverride; 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.</param> /// TextPayload that is a part of a full item link in chat.</param>
public ItemPayload(uint itemId, ItemKind kind = ItemKind.Normal, string? displayNameOverride = null) public ItemPayload(uint itemId, ItemKind kind = ItemKind.Normal, string? displayNameOverride = null)
{ {
this.itemId = itemId;
this.Kind = kind; this.Kind = kind;
this.rawItemId = itemId;
if (kind != ItemKind.EventItem)
this.rawItemId += (uint)kind;
this.displayName = displayNameOverride; this.displayName = displayNameOverride;
} }
@ -112,10 +120,16 @@ namespace Dalamud.Game.Text.SeStringHandling.Payloads
} }
/// <summary> /// <summary>
/// Gets the raw item ID of this payload. /// Gets the actual item ID of this payload.
/// </summary> /// </summary>
[JsonIgnore] [JsonIgnore]
public uint ItemId => this.itemId; public uint ItemId => GetAdjustedId(this.rawItemId).ItemId;
/// <summary>
/// Gets the raw, unadjusted item ID of this payload.
/// </summary>
[JsonIgnore]
public uint RawItemId => this.rawItemId;
/// <summary> /// <summary>
/// Gets the underlying Lumina Item represented by this payload. /// 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. /// The value is evaluated lazily and cached.
/// </remarks> /// </remarks>
[JsonIgnore] [JsonIgnore]
public Item? Item => this.item ??= this.DataResolver.GetExcelSheet<Item>()!.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<Item>()!.GetRow(this.ItemId);
return this.item;
}
}
/// <summary> /// <summary>
/// Gets a value indicating whether or not this item link is for a high-quality version of the 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
/// <inheritdoc/> /// <inheritdoc/>
public override string ToString() 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}";
} }
/// <inheritdoc/> /// <inheritdoc/>
protected override byte[] EncodeImpl() protected override byte[] EncodeImpl()
{ {
var actualItemId = this.itemId - (uint)this.Kind; var idBytes = MakeInteger(this.rawItemId);
var idBytes = MakeInteger(actualItemId);
var hasName = !string.IsNullOrEmpty(this.displayName); var hasName = !string.IsNullOrEmpty(this.displayName);
var chunkLen = idBytes.Length + 4; var chunkLen = idBytes.Length + 4;
@ -218,10 +244,8 @@ namespace Dalamud.Game.Text.SeStringHandling.Payloads
/// <inheritdoc/> /// <inheritdoc/>
protected override void DecodeImpl(BinaryReader reader, long endOfStream) protected override void DecodeImpl(BinaryReader reader, long endOfStream)
{ {
var (id, kind) = GetAdjustedId(GetInteger(reader)); this.rawItemId = GetInteger(reader);
this.Kind = GetAdjustedId(this.rawItemId).Kind;
this.itemId = id;
this.Kind = kind;
if (reader.BaseStream.Position + 3 < endOfStream) 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), > 500_000 and < 1_000_000 => (rawItemId - 500_000, ItemKind.Collectible),
> 1_000_000 and < 2_000_000 => (rawItemId - 1_000_000, ItemKind.Hq), > 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), _ => (rawItemId, ItemKind.Normal),
}; };
} }

View file

@ -13,14 +13,14 @@ namespace Dalamud.Game.Text.SeStringHandling.Payloads
public class TextPayload : Payload, ITextProvider public class TextPayload : Payload, ITextProvider
{ {
[JsonProperty] [JsonProperty]
private string text; private string? text;
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="TextPayload"/> class. /// Initializes a new instance of the <see cref="TextPayload"/> class.
/// Creates a new TextPayload for the given text. /// Creates a new TextPayload for the given text.
/// </summary> /// </summary>
/// <param name="text">The text to include for this payload.</param> /// <param name="text">The text to include for this payload.</param>
public TextPayload(string text) public TextPayload(string? text)
{ {
this.text = text; this.text = text;
} }
@ -41,12 +41,9 @@ namespace Dalamud.Game.Text.SeStringHandling.Payloads
/// This may contain SE's special unicode characters. /// This may contain SE's special unicode characters.
/// </summary> /// </summary>
[JsonIgnore] [JsonIgnore]
public string Text public string? Text
{ {
get get => this.text;
{
return this.text;
}
set set
{ {

View file

@ -156,22 +156,58 @@ namespace Dalamud.Game.Text.SeStringHandling
/// <param name="isHq">Whether to link the high-quality variant of the item.</param> /// <param name="isHq">Whether to link the high-quality variant of the item.</param>
/// <param name="displayNameOverride">An optional name override to display, instead of the actual item name.</param> /// <param name="displayNameOverride">An optional name override to display, instead of the actual item name.</param>
/// <returns>An SeString containing all the payloads necessary to display an item link in the chat log.</returns> /// <returns>An SeString containing all the payloads necessary to display an item link in the chat log.</returns>
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);
/// <summary>
/// Creates an SeString representing an entire Payload chain that can be used to link an item in the chat log.
/// </summary>
/// <param name="itemId">The id of the item to link.</param>
/// <param name="kind">The kind of item to link.</param>
/// <param name="displayNameOverride">An optional name override to display, instead of the actual item name.</param>
/// <returns>An SeString containing all the payloads necessary to display an item link in the chat log.</returns>
public static SeString CreateItemLink(uint itemId, ItemPayload.ItemKind kind = ItemPayload.ItemKind.Normal, string? displayNameOverride = null)
{ {
var data = Service<DataManager>.Get(); var data = Service<DataManager>.Get();
var displayName = displayNameOverride ?? data.GetExcelSheet<Item>()?.GetRow(itemId)?.Name; var displayName = displayNameOverride;
if (isHq) if (displayName == null)
{
switch (kind)
{
case ItemPayload.ItemKind.Normal:
case ItemPayload.ItemKind.Collectible:
case ItemPayload.ItemKind.Hq:
displayName = data.GetExcelSheet<Item>()?.GetRow(itemId)?.Name;
break;
case ItemPayload.ItemKind.EventItem:
displayName = data.GetExcelSheet<EventItem>()?.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}"; 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 // TODO: probably a cleaner way to build these than doing the bulk+insert
var payloads = new List<Payload>(new Payload[] var payloads = new List<Payload>(new Payload[]
{ {
new UIForegroundPayload(0x0225), new UIForegroundPayload(0x0225),
new UIGlowPayload(0x0226), new UIGlowPayload(0x0226),
new ItemPayload(itemId, isHq), new ItemPayload(itemId, kind),
// arrow goes here // arrow goes here
new TextPayload(displayName), new TextPayload(displayName),
RawPayload.LinkTerminator, RawPayload.LinkTerminator,

View file

@ -116,6 +116,14 @@ namespace Dalamud.Game.Text.SeStringHandling
public SeStringBuilder AddItemLink(uint itemId, ItemPayload.ItemKind kind, string? itemNameOverride = null) => public SeStringBuilder AddItemLink(uint itemId, ItemPayload.ItemKind kind, string? itemNameOverride = null) =>
this.Add(new ItemPayload(itemId, kind, itemNameOverride)); this.Add(new ItemPayload(itemId, kind, itemNameOverride));
/// <summary>
/// Add an item link to the builder.
/// </summary>
/// <param name="rawItemId">The raw item ID.</param>
/// <returns>The current builder.</returns>
public SeStringBuilder AddItemLinkRaw(uint rawItemId) =>
this.Add(ItemPayload.FromRaw(rawItemId));
/// <summary> /// <summary>
/// Add italicized raw text to the builder. /// Add italicized raw text to the builder.
/// </summary> /// </summary>

View file

@ -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
{
/// <summary>
/// Test setup for item payloads.
/// </summary>
internal class ItemPayloadAgingStep : IAgingStep
{
private SubStep currentSubStep;
private enum SubStep
{
PrintNormalItem,
HoverNormalItem,
PrintHqItem,
HoverHqItem,
PrintCollectable,
HoverCollectable,
PrintEventItem,
HoverEventItem,
PrintNormalWithText,
HoverNormalWithText,
Done,
}
/// <inheritdoc/>
public string Name => "Item Payloads";
/// <inheritdoc/>
public SelfTestStepResult RunStep()
{
var gameGui = Service<GameGui>.Get();
var chatGui = Service<ChatGui>.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;
}
/// <inheritdoc/>
public void CleanUp()
{
this.currentSubStep = SubStep.PrintNormalItem;
}
}
}

View file

@ -1,5 +1,6 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics;
using System.Linq; using System.Linq;
using System.Numerics; using System.Numerics;
@ -26,6 +27,7 @@ namespace Dalamud.Interface.Internal.Windows.SelfTest
new LoginEventAgingStep(), new LoginEventAgingStep(),
new WaitFramesAgingStep(1000), new WaitFramesAgingStep(1000),
new EnterTerritoryAgingStep(148, "Central Shroud"), new EnterTerritoryAgingStep(148, "Central Shroud"),
new ItemPayloadAgingStep(),
new ActorTableAgingStep(), new ActorTableAgingStep(),
new FateTableAgingStep(), new FateTableAgingStep(),
new AetheryteListAgingStep(), new AetheryteListAgingStep(),