Dalamud/Dalamud/Game/Text/SeStringHandling/Payloads/ItemPayload.cs
Haselnussbomber fdbfdbb2cd
Add SeStringEvaluator service (#2188)
* Add SeStringEvaluator service

* Move DrawCopyableText into WidgetUtil

* Use Icon2RemapTable in SeStringRenderer

* Beautify some code

* Make sure to use the correct language

* Add SeString Creator widget

* Fix getting local parameters

* Update expressionNames

* misc changes

* Use InvariantCulture in TryResolveSheet

* Add SeStringEvaluatorAgingStep

* Fix item id comparisons

* Add SheetRedirectResolverAgingStep

* Add NounProcessorAgingStep

* Update SeString.CreateItemLink

This also adds the internal ItemUtil class.

* Fix name of SeStringCreator widget

* Add Global Parameters tab to SeStringCreatorWidget

* Load widgets on demand

* Update SeStringCreatorWidget

* Resizable SeStringCreatorWidget panels

* Update GamepadStateAgingStep

* Experimental status was removed in #2144

* Update SheetRedirectResolver, rewrite Noun params

* Fixes for 4 am code

* Remove incorrect column offset

I have no idea how that happened.

* Draw names of linked things

---------

Co-authored-by: Soreepeong <3614868+Soreepeong@users.noreply.github.com>
Co-authored-by: KazWolfe <KazWolfe@users.noreply.github.com>
2025-03-24 09:00:27 -07:00

261 lines
8.9 KiB
C#

using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using Dalamud.Data;
using Dalamud.Utility;
using Lumina.Excel;
using Lumina.Excel.Sheets;
using Newtonsoft.Json;
namespace Dalamud.Game.Text.SeStringHandling.Payloads;
/// <summary>
/// An SeString Payload representing an interactable item link.
/// </summary>
public class ItemPayload : Payload
{
// mainly to allow overriding the name (for things like owo)
// TODO: even though this is present in some item links, it may not really have a use at all
// For things like owo, changing the text payload is probably correct, whereas changing the
// actual embedded name might not work properly.
private string? displayName;
[JsonProperty]
private uint rawItemId;
/// <summary>
/// Initializes a new instance of the <see cref="ItemPayload"/> class.
/// Creates a payload representing an interactable item link for the specified item.
/// </summary>
/// <param name="itemId">The id of the item.</param>
/// <param name="isHq">Whether or not the link should be for the high-quality variant of the item.</param>
/// <param name="displayNameOverride">An optional name to include in the item link. Typically this should
/// be left as null, or set to the normal item name. Actual overrides are better done with the subsequent
/// TextPayload that is a part of a full item link in chat.</param>
public ItemPayload(uint itemId, bool isHq, string? displayNameOverride = null)
{
this.rawItemId = itemId;
if (isHq)
this.rawItemId += (uint)ItemKind.Hq;
this.Kind = isHq ? ItemKind.Hq : ItemKind.Normal;
this.displayName = displayNameOverride;
}
/// <summary>
/// Initializes a new instance of the <see cref="ItemPayload"/> class.
/// Creates a payload representing an interactable item link for the specified item.
/// </summary>
/// <param name="itemId">The id of the item.</param>
/// <param name="kind">Kind of item to encode.</param>
/// <param name="displayNameOverride">An optional name to include in the item link. Typically this should
/// be left as null, or set to the normal item name. Actual overrides are better done with the subsequent
/// 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.Kind = kind;
this.rawItemId = itemId;
if (kind != ItemKind.EventItem)
this.rawItemId += (uint)kind;
this.displayName = displayNameOverride;
}
/// <summary>
/// Initializes a new instance of the <see cref="ItemPayload"/> class.
/// Creates a payload representing an interactable item link for the specified item.
/// </summary>
internal ItemPayload()
{
}
/// <summary>
/// Kinds of items that can be fetched from this payload.
/// </summary>
[Api12ToDo("Move this out of ItemPayload. It's used in other classes too.")]
public enum ItemKind : uint
{
/// <summary>
/// Normal items.
/// </summary>
Normal,
/// <summary>
/// Collectible Items.
/// </summary>
Collectible = 500_000,
/// <summary>
/// High-Quality items.
/// </summary>
Hq = 1_000_000,
/// <summary>
/// Event/Key items.
/// </summary>
EventItem = 2_000_000,
}
/// <inheritdoc/>
public override PayloadType Type => PayloadType.Item;
/// <summary>
/// Gets or sets the displayed name for this item link. Note that incoming links only sometimes have names embedded,
/// often the name is only present in a following text payload.
/// </summary>
public string? DisplayName
{
get
{
return this.displayName;
}
set
{
this.displayName = value;
this.Dirty = true;
}
}
/// <summary>
/// Gets the actual item ID of this payload.
/// </summary>
[JsonIgnore]
public uint ItemId => ItemUtil.GetBaseId(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 data represented by this payload. This is either a Item or EventItem <see cref="RowRef{T}"/>.
/// </summary>
[JsonIgnore]
public RowRef Item =>
this.Kind == ItemKind.EventItem
? (RowRef)LuminaUtils.CreateRef<EventItem>(this.ItemId)
: (RowRef)LuminaUtils.CreateRef<Item>(this.ItemId);
/// <summary>
/// Gets a value indicating whether or not this item link is for a high-quality version of the item.
/// </summary>
[JsonProperty]
public bool IsHQ => this.Kind == ItemKind.Hq;
/// <summary>
/// Gets or sets the kind of item represented by this payload.
/// </summary>
[JsonProperty]
public ItemKind Kind { get; set; } = ItemKind.Normal;
/// <summary>
/// Initializes a new instance of the <see cref="ItemPayload"/> class.
/// Creates a payload representing an interactable item link for the specified item.
/// </summary>
/// <param name="rawItemId">The raw, unadjusted id of the item.</param>
/// <param name="displayNameOverride">An optional name to include in the item link. Typically this should
/// be left as null, or set to the normal item name. Actual overrides are better done with the subsequent
/// TextPayload that is a part of a full item link in chat.</param>
/// <returns>The created item payload.</returns>
public static ItemPayload FromRaw(uint rawItemId, string? displayNameOverride = null)
{
var (id, kind) = ItemUtil.GetBaseId(rawItemId);
return new ItemPayload(id, kind, displayNameOverride);
}
/// <inheritdoc/>
public override string ToString()
{
var name = this.displayName ?? (this.Item.GetValueOrDefault<Item>()?.Name ?? this.Item.GetValueOrDefault<EventItem>()?.Name)?.ExtractText();
return $"{this.Type} - ItemId: {this.ItemId}, Kind: {this.Kind}, Name: {name}";
}
/// <inheritdoc/>
protected override byte[] EncodeImpl()
{
var idBytes = MakeInteger(this.rawItemId);
var hasName = !string.IsNullOrEmpty(this.displayName);
var chunkLen = idBytes.Length + 4;
if (hasName)
{
// 1 additional unknown byte compared to the nameless version, 1 byte for the name length, and then the name itself
chunkLen += 1 + 1 + this.displayName.Length;
if (this.IsHQ)
{
chunkLen += 4; // unicode representation of the HQ symbol is 3 bytes, preceded by a space
}
}
var bytes = new List<byte>()
{
START_BYTE,
(byte)SeStringChunkType.Interactable, (byte)chunkLen, (byte)EmbeddedInfoType.ItemLink,
};
bytes.AddRange(idBytes);
// unk
bytes.AddRange(new byte[] { 0x02, 0x01 });
// Links don't have to include the name, but if they do, it requires additional work
if (hasName)
{
var nameLen = this.displayName.Length + 1;
if (this.IsHQ)
{
nameLen += 4; // space plus 3 bytes for HQ symbol
}
bytes.AddRange(new byte[]
{
0xFF, // unk
(byte)nameLen,
});
bytes.AddRange(Encoding.UTF8.GetBytes(this.displayName));
if (this.IsHQ)
{
// space and HQ symbol
bytes.AddRange(new byte[] { 0x20, 0xEE, 0x80, 0xBC });
}
}
bytes.Add(END_BYTE);
return bytes.ToArray();
}
/// <inheritdoc/>
protected override void DecodeImpl(BinaryReader reader, long endOfStream)
{
this.rawItemId = GetInteger(reader);
this.Kind = ItemUtil.GetBaseId(this.rawItemId).Kind;
if (reader.BaseStream.Position + 3 < endOfStream)
{
// unk
reader.ReadBytes(3);
var itemNameLen = (int)GetInteger(reader);
var itemNameBytes = reader.ReadBytes(itemNameLen);
// it probably isn't necessary to store this, as we now get the lumina Item
// on demand from the id, which will have the name
// For incoming links, the name "should?" always match
// but we'll store it for use in encode just in case it doesn't
// HQ items have the HQ symbol as part of the name, but since we already recorded
// the HQ flag, we want just the bare name
if (this.IsHQ)
{
itemNameBytes = itemNameBytes.Take(itemNameLen - 4).ToArray();
}
this.displayName = Encoding.UTF8.GetString(itemNameBytes);
}
}
}