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.Internal;
using Dalamud.Utility;
using FFXIVClientStructs.FFXIV.Client.System.String;
using ImGuiNET;
using Serilog;
@ -21,7 +22,7 @@ namespace Dalamud.Game.Gui
/// </summary>
[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<HandleActionOutDelegate> handleActionOutHook;
private readonly Hook<HandleImmDelegate> handleImmHook;
private readonly Hook<ToggleUiHideDelegate> toggleUiHideHook;
private readonly Hook<Utf8StringFromSequenceDelegate> utf8StringFromSequenceHook;
private GetUIMapObjectDelegate getUIMapObject;
private OpenMapWithFlagDelegate openMapWithFlag;
@ -79,6 +81,8 @@ namespace Dalamud.Game.Gui
this.toggleUiHideHook = new Hook<ToggleUiHideDelegate>(this.address.ToggleUiHide, this.ToggleUiHideDetour);
this.getAgentModule = Marshal.GetDelegateForFunctionPointer<GetAgentModuleDelegate>(this.address.GetAgentModule);
this.utf8StringFromSequenceHook = new Hook<Utf8StringFromSequenceDelegate>(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();
}
/// <summary>
@ -457,6 +465,7 @@ namespace Dalamud.Game.Gui
this.toggleUiHideHook.Dispose();
this.handleActionHoverHook.Dispose();
this.handleActionOutHook.Dispose();
this.utf8StringFromSequenceHook.Dispose();
}
/// <summary>
@ -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?
}
}
}

View file

@ -76,6 +76,11 @@ namespace Dalamud.Game.Gui
/// </summary>
public IntPtr GetAgentModule { get; private set; }
/// <summary>
/// Gets the address of the native Utf8StringFromSequence method.
/// </summary>
public IntPtr Utf8StringFromSequence { get; private set; }
/// <inheritdoc/>
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);

View file

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

View file

@ -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;
/// <summary>
/// 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>
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.</param>
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
}
/// <summary>
/// Gets the raw item ID of this payload.
/// Gets the actual item ID of this payload.
/// </summary>
[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>
/// 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.
/// </remarks>
[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>
/// 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/>
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/>
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
/// <inheritdoc/>
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),
};
}

View file

@ -13,14 +13,14 @@ namespace Dalamud.Game.Text.SeStringHandling.Payloads
public class TextPayload : Payload, ITextProvider
{
[JsonProperty]
private string text;
private string? text;
/// <summary>
/// Initializes a new instance of the <see cref="TextPayload"/> class.
/// Creates a new TextPayload for the given text.
/// </summary>
/// <param name="text">The text to include for this payload.</param>
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.
/// </summary>
[JsonIgnore]
public string Text
public string? Text
{
get
{
return this.text;
}
get => this.text;
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="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, 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 displayName = displayNameOverride ?? data.GetExcelSheet<Item>()?.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<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}";
}
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<Payload>(new Payload[]
{
new UIForegroundPayload(0x0225),
new UIGlowPayload(0x0226),
new ItemPayload(itemId, isHq),
new ItemPayload(itemId, kind),
// arrow goes here
new TextPayload(displayName),
RawPayload.LinkTerminator,

View file

@ -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));
/// <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>
/// Add italicized raw text to the builder.
/// </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.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(),