using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices;
using System.Text;
using Dalamud.Data;
using Dalamud.Game.Text.Evaluator;
using Dalamud.Game.Text.SeStringHandling.Payloads;
using Dalamud.Utility;
using Lumina.Excel.Sheets;
using Newtonsoft.Json;
using LSeStringBuilder = Lumina.Text.SeStringBuilder;
namespace Dalamud.Game.Text.SeStringHandling;
///
/// This class represents a parsed SeString.
///
public class SeString
{
///
/// Initializes a new instance of the class.
/// Creates a new SeString from an ordered list of payloads.
///
public SeString()
{
this.Payloads = new List();
}
///
/// Initializes a new instance of the class.
/// Creates a new SeString from an ordered list of payloads.
///
/// The Payload objects to make up this string.
[JsonConstructor]
public SeString(List payloads)
{
this.Payloads = payloads;
}
///
/// Initializes a new instance of the class.
/// Creates a new SeString from an ordered list of payloads.
///
/// The Payload objects to make up this string.
public SeString(params Payload[] payloads)
{
this.Payloads = new List(payloads);
}
///
/// Gets a list of Payloads necessary to display the arrow link marker icon in chat
/// with the appropriate glow and coloring.
///
/// A list of all the payloads required to insert the link marker.
public static IEnumerable TextArrowPayloads
{
get
{
var clientState = Service.Get();
var markerSpace = clientState.ClientLanguage switch
{
ClientLanguage.German => " ",
ClientLanguage.French => " ",
_ => string.Empty,
};
return new List
{
new UIForegroundPayload(500),
new UIGlowPayload(501),
new TextPayload($"{(char)SeIconChar.LinkMarker}{markerSpace}"),
UIGlowPayload.UIGlowOff,
UIForegroundPayload.UIForegroundOff,
};
}
}
///
/// Gets an empty SeString.
///
public static SeString Empty => new();
///
/// Gets the ordered list of payloads included in this SeString.
///
public List Payloads { get; }
///
/// Gets all of the raw text from a message as a single joined string.
///
///
/// All the raw text from the contained payloads, joined into a single string.
///
public string TextValue
{
get
{
return this.Payloads
.Where(p => p is ITextProvider)
.Cast()
.Aggregate(new StringBuilder(), (sb, tp) => sb.Append(tp.Text), sb => sb.ToString());
}
}
///
/// Implicitly convert a string into a SeString containing a .
///
/// string to convert.
/// Equivalent SeString.
public static implicit operator SeString(string str) => new(new TextPayload(str));
///
/// Implicitly convert a string into a SeString containing a .
///
/// string to convert.
/// Equivalent SeString.
public static explicit operator SeString(Lumina.Text.SeString str) => str.ToDalamudString();
///
/// Parse a binary game message into an SeString.
///
/// Pointer to the string's data in memory.
/// Length of the string's data in memory.
/// An SeString containing parsed Payload objects for each payload in the data.
public static unsafe SeString Parse(byte* ptr, int len)
{
if (ptr == null)
return Empty;
var payloads = new List();
using (var stream = new UnmanagedMemoryStream(ptr, len))
using (var reader = new BinaryReader(stream))
{
while (stream.Position < len)
{
payloads.Add(Payload.Decode(reader));
}
}
return new SeString(payloads);
}
///
/// Parse a binary game message into an SeString.
///
/// Binary message payload data in SE's internal format.
/// An SeString containing parsed Payload objects for each payload in the data.
public static unsafe SeString Parse(ReadOnlySpan data)
{
fixed (byte* ptr = data)
{
var len = data.IndexOf((byte)0);
return Parse(ptr, len == -1 ? data.Length : len);
}
}
///
/// Parse a binary game message into an SeString.
///
/// Binary message payload data in SE's internal format.
/// An SeString containing parsed Payload objects for each payload in the data.
public static SeString Parse(byte[] bytes) => Parse(new ReadOnlySpan(bytes));
///
/// Parse a binary game message into an SeString.
///
/// Pointer to the string's data in memory. Needs to be null-terminated.
/// An SeString containing parsed Payload objects for each payload in the data.
public static unsafe SeString Parse(byte* ptr) => Parse(MemoryMarshal.CreateReadOnlySpanFromNullTerminated(ptr));
///
/// 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.
/// 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) =>
CreateItemLink(itemId, isHq ? ItemKind.Hq : 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, ItemKind kind = ItemKind.Normal, string? displayNameOverride = null)
{
var clientState = Service.Get();
var seStringEvaluator = Service.Get();
var rawId = ItemUtil.GetRawId(itemId, kind);
var displayName = displayNameOverride ?? ItemUtil.GetItemName(rawId);
if (displayName.IsEmpty)
throw new Exception("Invalid item ID specified, could not determine item name.");
var copyName = ItemUtil.GetItemName(rawId, false).ExtractText();
var textColor = ItemUtil.GetItemRarityColorType(rawId);
var textEdgeColor = textColor + 1u;
var sb = LSeStringBuilder.SharedPool.Get();
var itemLink = sb
.PushColorType(textColor)
.PushEdgeColorType(textEdgeColor)
.PushLinkItem(rawId, copyName)
.Append(displayName)
.PopLink()
.PopEdgeColorType()
.PopColorType()
.ToReadOnlySeString();
LSeStringBuilder.SharedPool.Return(sb);
return SeString.Parse(seStringEvaluator.EvaluateFromAddon(371, [itemLink], clientState.ClientLanguage));
}
///
/// Creates an SeString representing an entire Payload chain that can be used to link an item in the chat log.
///
/// The Lumina Item to link.
/// 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(Item item, bool isHq, string? displayNameOverride = null)
{
return CreateItemLink(item.RowId, isHq, displayNameOverride ?? item.Name.ExtractText());
}
///
/// Creates an SeString representing an entire Payload chain that can be used to link a map position in the chat log.
///
/// The id of the TerritoryType for this map link.
/// The id of the Map for this map link.
/// The raw x-coordinate for this link.
/// The raw y-coordinate for this link..
/// An SeString containing all of the payloads necessary to display a map link in the chat log.
public static SeString CreateMapLink(uint territoryId, uint mapId, int rawX, int rawY) =>
CreateMapLinkWithInstance(territoryId, mapId, null, rawX, rawY);
///
/// Creates an SeString representing an entire Payload chain that can be used to link a map position in the chat log.
///
/// The id of the TerritoryType for this map link.
/// The id of the Map for this map link.
/// An optional area instance number to be included in this link.
/// The raw x-coordinate for this link.
/// The raw y-coordinate for this link..
/// An SeString containing all of the payloads necessary to display a map link in the chat log.
public static SeString CreateMapLinkWithInstance(uint territoryId, uint mapId, int? instance, int rawX, int rawY)
{
var mapPayload = new MapLinkPayload(territoryId, mapId, rawX, rawY);
var nameString = GetMapLinkNameString(mapPayload.PlaceName, instance, mapPayload.CoordinateString);
var payloads = new List(new Payload[]
{
mapPayload,
// arrow goes here
new TextPayload(nameString),
RawPayload.LinkTerminator,
});
payloads.InsertRange(1, TextArrowPayloads);
return new SeString(payloads);
}
///
/// Creates an SeString representing an entire Payload chain that can be used to link a map position in the chat log.
///
/// The id of the TerritoryType for this map link.
/// The id of the Map for this map link.
/// The human-readable x-coordinate for this link.
/// The human-readable y-coordinate for this link.
/// An optional offset to account for rounding and truncation errors; it is best to leave this untouched in most cases.
/// An SeString containing all of the payloads necessary to display a map link in the chat log.
public static SeString CreateMapLink(
uint territoryId, uint mapId, float xCoord, float yCoord, float fudgeFactor = 0.05f) =>
CreateMapLinkWithInstance(territoryId, mapId, null, xCoord, yCoord, fudgeFactor);
///
/// Creates an SeString representing an entire Payload chain that can be used to link a map position in the chat log.
///
/// The id of the TerritoryType for this map link.
/// The id of the Map for this map link.
/// An optional area instance number to be included in this link.
/// The human-readable x-coordinate for this link.
/// The human-readable y-coordinate for this link.
/// An optional offset to account for rounding and truncation errors; it is best to leave this untouched in most cases.
/// An SeString containing all of the payloads necessary to display a map link in the chat log.
public static SeString CreateMapLinkWithInstance(uint territoryId, uint mapId, int? instance, float xCoord, float yCoord, float fudgeFactor = 0.05f)
{
var mapPayload = new MapLinkPayload(territoryId, mapId, xCoord, yCoord, fudgeFactor);
var nameString = GetMapLinkNameString(mapPayload.PlaceName, instance, mapPayload.CoordinateString);
var payloads = new List(new Payload[]
{
mapPayload,
// arrow goes here
new TextPayload(nameString),
RawPayload.LinkTerminator,
});
payloads.InsertRange(1, TextArrowPayloads);
return new SeString(payloads);
}
///
/// Creates an SeString representing an entire Payload chain that can be used to link a map position in the chat log, matching a specified zone name.
/// Returns null if no corresponding PlaceName was found.
///
/// The name of the location for this link. This should be exactly the name as seen in a displayed map link in-game for the same zone.
/// The human-readable x-coordinate for this link.
/// The human-readable y-coordinate for this link.
/// An optional offset to account for rounding and truncation errors; it is best to leave this untouched in most cases.
/// An SeString containing all of the payloads necessary to display a map link in the chat log.
public static SeString? CreateMapLink(string placeName, float xCoord, float yCoord, float fudgeFactor = 0.05f) =>
CreateMapLinkWithInstance(placeName, null, xCoord, yCoord, fudgeFactor);
///
/// Creates an SeString representing an entire Payload chain that can be used to link a map position in the chat log, matching a specified zone name.
/// Returns null if no corresponding PlaceName was found.
///
/// The name of the location for this link. This should be exactly the name as seen in a displayed map link in-game for the same zone.
/// An optional area instance number to be included in this link.
/// The human-readable x-coordinate for this link.
/// The human-readable y-coordinate for this link.
/// An optional offset to account for rounding and truncation errors; it is best to leave this untouched in most cases.
/// An SeString containing all of the payloads necessary to display a map link in the chat log.
public static SeString? CreateMapLinkWithInstance(string placeName, int? instance, float xCoord, float yCoord, float fudgeFactor = 0.05f)
{
var data = Service.Get();
var mapSheet = data.GetExcelSheet