Revert "refactor(Dalamud): switch to file-scoped namespaces"

This reverts commit b5f34c3199.
This commit is contained in:
goat 2021-11-18 15:23:40 +01:00
parent d473826247
commit 1561fbac00
No known key found for this signature in database
GPG key ID: 7773BB5B43BA52E5
325 changed files with 45549 additions and 45209 deletions

View file

@ -1,39 +1,40 @@
using System.Collections.Generic;
namespace Dalamud.Game.Text.Sanitizer;
/// <summary>
/// Sanitize strings to remove soft hyphens and other special characters.
/// </summary>
public interface ISanitizer
namespace Dalamud.Game.Text.Sanitizer
{
/// <summary>
/// Creates a sanitized string using current clientLanguage.
/// Sanitize strings to remove soft hyphens and other special characters.
/// </summary>
/// <param name="unsanitizedString">An unsanitized string to sanitize.</param>
/// <returns>A sanitized string.</returns>
string Sanitize(string unsanitizedString);
public interface ISanitizer
{
/// <summary>
/// Creates a sanitized string using current clientLanguage.
/// </summary>
/// <param name="unsanitizedString">An unsanitized string to sanitize.</param>
/// <returns>A sanitized string.</returns>
string Sanitize(string unsanitizedString);
/// <summary>
/// Creates a sanitized string using request clientLanguage.
/// </summary>
/// <param name="unsanitizedString">An unsanitized string to sanitize.</param>
/// <param name="clientLanguage">Target language for sanitized strings.</param>
/// <returns>A sanitized string.</returns>
string Sanitize(string unsanitizedString, ClientLanguage clientLanguage);
/// <summary>
/// Creates a sanitized string using request clientLanguage.
/// </summary>
/// <param name="unsanitizedString">An unsanitized string to sanitize.</param>
/// <param name="clientLanguage">Target language for sanitized strings.</param>
/// <returns>A sanitized string.</returns>
string Sanitize(string unsanitizedString, ClientLanguage clientLanguage);
/// <summary>
/// Creates a list of sanitized strings using current clientLanguage.
/// </summary>
/// <param name="unsanitizedStrings">List of unsanitized string to sanitize.</param>
/// <returns>A list of sanitized strings.</returns>
IEnumerable<string> Sanitize(IEnumerable<string> unsanitizedStrings);
/// <summary>
/// Creates a list of sanitized strings using current clientLanguage.
/// </summary>
/// <param name="unsanitizedStrings">List of unsanitized string to sanitize.</param>
/// <returns>A list of sanitized strings.</returns>
IEnumerable<string> Sanitize(IEnumerable<string> unsanitizedStrings);
/// <summary>
/// Creates a list of sanitized strings using requested clientLanguage.
/// </summary>
/// <param name="unsanitizedStrings">List of unsanitized string to sanitize.</param>
/// <param name="clientLanguage">Target language for sanitized strings.</param>
/// <returns>A list of sanitized strings.</returns>
IEnumerable<string> Sanitize(IEnumerable<string> unsanitizedStrings, ClientLanguage clientLanguage);
/// <summary>
/// Creates a list of sanitized strings using requested clientLanguage.
/// </summary>
/// <param name="unsanitizedStrings">List of unsanitized string to sanitize.</param>
/// <param name="clientLanguage">Target language for sanitized strings.</param>
/// <returns>A list of sanitized strings.</returns>
IEnumerable<string> Sanitize(IEnumerable<string> unsanitizedStrings, ClientLanguage clientLanguage);
}
}

View file

@ -2,109 +2,110 @@ using System;
using System.Collections.Generic;
using System.Linq;
namespace Dalamud.Game.Text.Sanitizer;
/// <summary>
/// Sanitize strings to remove soft hyphens and other special characters.
/// </summary>
public class Sanitizer : ISanitizer
namespace Dalamud.Game.Text.Sanitizer
{
private static readonly Dictionary<string, string> DESanitizationDict = new()
{
{ "\u0020\u2020", string.Empty }, // dagger
};
private static readonly Dictionary<string, string> FRSanitizationDict = new()
{
{ "\u0153", "\u006F\u0065" }, // ligature oe
};
private readonly ClientLanguage defaultClientLanguage;
/// <summary>
/// Initializes a new instance of the <see cref="Sanitizer"/> class.
/// Sanitize strings to remove soft hyphens and other special characters.
/// </summary>
/// <param name="defaultClientLanguage">Default clientLanguage for sanitizing strings.</param>
public Sanitizer(ClientLanguage defaultClientLanguage)
public class Sanitizer : ISanitizer
{
this.defaultClientLanguage = defaultClientLanguage;
}
/// <summary>
/// Creates a sanitized string using current clientLanguage.
/// </summary>
/// <param name="unsanitizedString">An unsanitized string to sanitize.</param>
/// <returns>A sanitized string.</returns>
public string Sanitize(string unsanitizedString)
{
return SanitizeByLanguage(unsanitizedString, this.defaultClientLanguage);
}
/// <summary>
/// Creates a sanitized string using request clientLanguage.
/// </summary>
/// <param name="unsanitizedString">An unsanitized string to sanitize.</param>
/// <param name="clientLanguage">Target language for sanitized strings.</param>
/// <returns>A sanitized string.</returns>
public string Sanitize(string unsanitizedString, ClientLanguage clientLanguage)
{
return SanitizeByLanguage(unsanitizedString, clientLanguage);
}
/// <summary>
/// Creates a list of sanitized strings using current clientLanguage.
/// </summary>
/// <param name="unsanitizedStrings">List of unsanitized string to sanitize.</param>
/// <returns>A list of sanitized strings.</returns>
public IEnumerable<string> Sanitize(IEnumerable<string> unsanitizedStrings)
{
return SanitizeByLanguage(unsanitizedStrings, this.defaultClientLanguage);
}
/// <summary>
/// Creates a list of sanitized strings using requested clientLanguage.
/// </summary>
/// <param name="unsanitizedStrings">List of unsanitized string to sanitize.</param>
/// <param name="clientLanguage">Target language for sanitized strings.</param>
/// <returns>A list of sanitized strings.</returns>
public IEnumerable<string> Sanitize(IEnumerable<string> unsanitizedStrings, ClientLanguage clientLanguage)
{
return SanitizeByLanguage(unsanitizedStrings, clientLanguage);
}
private static string SanitizeByLanguage(string unsanitizedString, ClientLanguage clientLanguage)
{
var sanitizedString = FilterUnprintableCharacters(unsanitizedString);
return clientLanguage switch
private static readonly Dictionary<string, string> DESanitizationDict = new()
{
ClientLanguage.Japanese or ClientLanguage.English => sanitizedString,
ClientLanguage.German => FilterByDict(sanitizedString, DESanitizationDict),
ClientLanguage.French => FilterByDict(sanitizedString, FRSanitizationDict),
_ => throw new ArgumentOutOfRangeException(nameof(clientLanguage), clientLanguage, null),
{ "\u0020\u2020", string.Empty }, // dagger
};
}
private static IEnumerable<string> SanitizeByLanguage(IEnumerable<string> unsanitizedStrings, ClientLanguage clientLanguage)
{
return clientLanguage switch
private static readonly Dictionary<string, string> FRSanitizationDict = new()
{
ClientLanguage.Japanese => unsanitizedStrings.Select(FilterUnprintableCharacters),
ClientLanguage.English => unsanitizedStrings.Select(FilterUnprintableCharacters),
ClientLanguage.German => unsanitizedStrings.Select(original => FilterByDict(FilterUnprintableCharacters(original), DESanitizationDict)),
ClientLanguage.French => unsanitizedStrings.Select(original => FilterByDict(FilterUnprintableCharacters(original), FRSanitizationDict)),
_ => throw new ArgumentOutOfRangeException(nameof(clientLanguage), clientLanguage, null),
{ "\u0153", "\u006F\u0065" }, // ligature oe
};
}
private static string FilterUnprintableCharacters(string str)
{
return new string(str?.Where(ch => ch >= 0x20).ToArray());
}
private readonly ClientLanguage defaultClientLanguage;
private static string FilterByDict(string str, Dictionary<string, string> dict)
{
return dict.Aggregate(
str, (current, kvp) =>
current.Replace(kvp.Key, kvp.Value));
/// <summary>
/// Initializes a new instance of the <see cref="Sanitizer"/> class.
/// </summary>
/// <param name="defaultClientLanguage">Default clientLanguage for sanitizing strings.</param>
public Sanitizer(ClientLanguage defaultClientLanguage)
{
this.defaultClientLanguage = defaultClientLanguage;
}
/// <summary>
/// Creates a sanitized string using current clientLanguage.
/// </summary>
/// <param name="unsanitizedString">An unsanitized string to sanitize.</param>
/// <returns>A sanitized string.</returns>
public string Sanitize(string unsanitizedString)
{
return SanitizeByLanguage(unsanitizedString, this.defaultClientLanguage);
}
/// <summary>
/// Creates a sanitized string using request clientLanguage.
/// </summary>
/// <param name="unsanitizedString">An unsanitized string to sanitize.</param>
/// <param name="clientLanguage">Target language for sanitized strings.</param>
/// <returns>A sanitized string.</returns>
public string Sanitize(string unsanitizedString, ClientLanguage clientLanguage)
{
return SanitizeByLanguage(unsanitizedString, clientLanguage);
}
/// <summary>
/// Creates a list of sanitized strings using current clientLanguage.
/// </summary>
/// <param name="unsanitizedStrings">List of unsanitized string to sanitize.</param>
/// <returns>A list of sanitized strings.</returns>
public IEnumerable<string> Sanitize(IEnumerable<string> unsanitizedStrings)
{
return SanitizeByLanguage(unsanitizedStrings, this.defaultClientLanguage);
}
/// <summary>
/// Creates a list of sanitized strings using requested clientLanguage.
/// </summary>
/// <param name="unsanitizedStrings">List of unsanitized string to sanitize.</param>
/// <param name="clientLanguage">Target language for sanitized strings.</param>
/// <returns>A list of sanitized strings.</returns>
public IEnumerable<string> Sanitize(IEnumerable<string> unsanitizedStrings, ClientLanguage clientLanguage)
{
return SanitizeByLanguage(unsanitizedStrings, clientLanguage);
}
private static string SanitizeByLanguage(string unsanitizedString, ClientLanguage clientLanguage)
{
var sanitizedString = FilterUnprintableCharacters(unsanitizedString);
return clientLanguage switch
{
ClientLanguage.Japanese or ClientLanguage.English => sanitizedString,
ClientLanguage.German => FilterByDict(sanitizedString, DESanitizationDict),
ClientLanguage.French => FilterByDict(sanitizedString, FRSanitizationDict),
_ => throw new ArgumentOutOfRangeException(nameof(clientLanguage), clientLanguage, null),
};
}
private static IEnumerable<string> SanitizeByLanguage(IEnumerable<string> unsanitizedStrings, ClientLanguage clientLanguage)
{
return clientLanguage switch
{
ClientLanguage.Japanese => unsanitizedStrings.Select(FilterUnprintableCharacters),
ClientLanguage.English => unsanitizedStrings.Select(FilterUnprintableCharacters),
ClientLanguage.German => unsanitizedStrings.Select(original => FilterByDict(FilterUnprintableCharacters(original), DESanitizationDict)),
ClientLanguage.French => unsanitizedStrings.Select(original => FilterByDict(FilterUnprintableCharacters(original), FRSanitizationDict)),
_ => throw new ArgumentOutOfRangeException(nameof(clientLanguage), clientLanguage, null),
};
}
private static string FilterUnprintableCharacters(string str)
{
return new string(str?.Where(ch => ch >= 0x20).ToArray());
}
private static string FilterByDict(string str, Dictionary<string, string> dict)
{
return dict.Aggregate(
str, (current, kvp) =>
current.Replace(kvp.Key, kvp.Value));
}
}
}

File diff suppressed because it is too large Load diff

View file

@ -1,437 +1,438 @@
namespace Dalamud.Game.Text.SeStringHandling;
/// <summary>
/// This class represents special icons that can appear in chat naturally or as IconPayloads.
/// </summary>
public enum BitmapFontIcon : uint
namespace Dalamud.Game.Text.SeStringHandling
{
/// <summary>
/// No icon.
/// </summary>
None = 0,
/// <summary>
/// The controller D-pad up icon.
/// </summary>
ControllerDPadUp = 1,
/// <summary>
/// The controller D-pad down icon.
/// </summary>
ControllerDPadDown = 2,
/// <summary>
/// The controller D-pad left icon.
/// </summary>
ControllerDPadLeft = 3,
/// <summary>
/// The controller D-pad right icon.
/// </summary>
ControllerDPadRight = 4,
/// <summary>
/// The controller D-pad up/down icon.
/// </summary>
ControllerDPadUpDown = 5,
/// <summary>
/// The controller D-pad left/right icon.
/// </summary>
ControllerDPadLeftRight = 6,
/// <summary>
/// The controller D-pad all directions icon.
/// </summary>
ControllerDPadAll = 7,
/// <summary>
/// The controller button 0 icon (Xbox: B, PlayStation: Circle).
/// </summary>
ControllerButton0 = 8,
/// <summary>
/// The controller button 1 icon (XBox: A, PlayStation: Cross).
/// </summary>
ControllerButton1 = 9,
/// <summary>
/// The controller button 2 icon (XBox: X, PlayStation: Square).
/// </summary>
ControllerButton2 = 10,
/// <summary>
/// The controller button 3 icon (BBox: Y, PlayStation: Triangle).
/// </summary>
ControllerButton3 = 11,
/// <summary>
/// The controller left shoulder button icon.
/// </summary>
ControllerShoulderLeft = 12,
/// <summary>
/// The controller right shoulder button icon.
/// </summary>
ControllerShoulderRight = 13,
/// <summary>
/// The controller left trigger button icon.
/// </summary>
ControllerTriggerLeft = 14,
/// <summary>
/// The controller right trigger button icon.
/// </summary>
ControllerTriggerRight = 15,
/// <summary>
/// The controller left analog stick in icon.
/// </summary>
ControllerAnalogLeftStickIn = 16,
/// <summary>
/// The controller right analog stick in icon.
/// </summary>
ControllerAnalogRightStickIn = 17,
/// <summary>
/// The controller start button icon.
/// </summary>
ControllerStart = 18,
/// <summary>
/// The controller back button icon.
/// </summary>
ControllerBack = 19,
/// <summary>
/// The controller left analog stick icon.
/// </summary>
ControllerAnalogLeftStick = 20,
/// <summary>
/// The controller left analog stick up/down icon.
/// </summary>
ControllerAnalogLeftStickUpDown = 21,
/// <summary>
/// The controller left analog stick left/right icon.
/// </summary>
ControllerAnalogLeftStickLeftRight = 22,
/// <summary>
/// The controller right analog stick icon.
/// </summary>
ControllerAnalogRightStick = 23,
/// <summary>
/// The controller right analog stick up/down icon.
/// </summary>
ControllerAnalogRightStickUpDown = 24,
/// <summary>
/// The controller right analog stick left/right icon.
/// </summary>
ControllerAnalogRightStickLeftRight = 25,
/// <summary>
/// The La Noscea region icon.
/// </summary>
LaNoscea = 51,
/// <summary>
/// The Black Shroud region icon.
/// </summary>
BlackShroud = 52,
/// <summary>
/// The Thanalan region icon.
/// </summary>
Thanalan = 53,
/// <summary>
/// The auto translate begin icon.
/// </summary>
AutoTranslateBegin = 54,
/// <summary>
/// The auto translate end icon.
/// </summary>
AutoTranslateEnd = 55,
/// <summary>
/// The fire element icon.
/// </summary>
ElementFire = 56,
/// <summary>
/// The ice element icon.
/// </summary>
ElementIce = 57,
/// <summary>
/// The wind element icon.
/// </summary>
ElementWind = 58,
/// <summary>
/// The earth element icon.
/// </summary>
ElementEarth = 59,
/// <summary>
/// The lightning element icon.
/// </summary>
ElementLightning = 60,
/// <summary>
/// The water element icon.
/// </summary>
ElementWater = 61,
/// <summary>
/// The level sync icon.
/// </summary>
LevelSync = 62,
/// <summary>
/// The warning icon.
/// </summary>
Warning = 63,
/// <summary>
/// The Ishgard region icon.
/// </summary>
Ishgard = 64,
/// <summary>
/// The Aetheryte icon.
/// </summary>
Aetheryte = 65,
/// <summary>
/// The Aethernet icon.
/// </summary>
Aethernet = 66,
/// <summary>
/// The gold star icon.
/// </summary>
GoldStar = 67,
/// <summary>
/// The silver star icon.
/// </summary>
SilverStar = 68,
/// <summary>
/// The green dot icon.
/// </summary>
GreenDot = 70,
/// <summary>
/// The unsheathed sword icon.
/// </summary>
SwordUnsheathed = 71,
/// <summary>
/// The sheathed sword icon.
/// </summary>
SwordSheathed = 72,
/// <summary>
/// The dice icon.
/// </summary>
Dice = 73,
/// <summary>
/// The flyable zone icon.
/// </summary>
FlyZone = 74,
/// <summary>
/// The no-flying zone icon.
/// </summary>
FlyZoneLocked = 75,
/// <summary>
/// The no-circle/prohibited icon.
/// </summary>
NoCircle = 76,
/// <summary>
/// The sprout icon.
/// </summary>
NewAdventurer = 77,
/// <summary>
/// The mentor icon.
/// </summary>
Mentor = 78,
/// <summary>
/// The PvE mentor icon.
/// </summary>
MentorPvE = 79,
/// <summary>
/// The crafting mentor icon.
/// </summary>
MentorCrafting = 80,
/// <summary>
/// The PvP mentor icon.
/// </summary>
MentorPvP = 81,
/// <summary>
/// The tank role icon.
/// </summary>
Tank = 82,
/// <summary>
/// The healer role icon.
/// </summary>
Healer = 83,
/// <summary>
/// The DPS role icon.
/// </summary>
DPS = 84,
/// <summary>
/// The crafter role icon.
/// </summary>
Crafter = 85,
/// <summary>
/// The gatherer role icon.
/// </summary>
Gatherer = 86,
/// <summary>
/// The "any" role icon.
/// </summary>
AnyClass = 87,
/// <summary>
/// The cross-world icon.
/// </summary>
CrossWorld = 88,
/// <summary>
/// The slay type Fate icon.
/// </summary>
FateSlay = 89,
/// <summary>
/// The boss type Fate icon.
/// </summary>
FateBoss = 90,
/// <summary>
/// The gather type Fate icon.
/// </summary>
FateGather = 91,
/// <summary>
/// The defend type Fate icon.
/// </summary>
FateDefend = 92,
/// <summary>
/// The escort type Fate icon.
/// </summary>
FateEscort = 93,
/// <summary>
/// The special type 1 Fate icon.
/// </summary>
FateSpecial1 = 94,
/// <summary>
/// The returner icon.
/// </summary>
Returner = 95,
/// <summary>
/// The Far-East region icon.
/// </summary>
FarEast = 96,
/// <summary>
/// The Gyr Albania region icon.
/// </summary>
GyrAbania = 97,
/// <summary>
/// The special type 2 Fate icon.
/// </summary>
FateSpecial2 = 98,
/// <summary>
/// The priority world icon.
/// </summary>
PriorityWorld = 99,
/// <summary>
/// The elemental level icon.
/// </summary>
ElementalLevel = 100,
/// <summary>
/// The exclamation rectangle icon.
/// </summary>
ExclamationRectangle = 101,
/// <summary>
/// The notorious monster icon.
/// </summary>
NotoriousMonster = 102,
/// <summary>
/// The recording icon.
/// </summary>
Recording = 103,
/// <summary>
/// The alarm icon.
/// </summary>
Alarm = 104,
/// <summary>
/// The arrow up icon.
/// </summary>
ArrowUp = 105,
/// <summary>
/// The arrow down icon.
/// </summary>
ArrowDown = 106,
/// <summary>
/// The Crystarium region icon.
/// </summary>
Crystarium = 107,
/// <summary>
/// The mentor problem icon.
/// </summary>
MentorProblem = 108,
/// <summary>
/// The unknown gold type Fate icon.
/// </summary>
FateUnknownGold = 109,
/// <summary>
/// The orange diamond icon.
/// </summary>
OrangeDiamond = 110,
/// <summary>
/// The crafting type Fate icon.
/// </summary>
FateCrafting = 111,
/// This class represents special icons that can appear in chat naturally or as IconPayloads.
/// </summary>
public enum BitmapFontIcon : uint
{
/// <summary>
/// No icon.
/// </summary>
None = 0,
/// <summary>
/// The controller D-pad up icon.
/// </summary>
ControllerDPadUp = 1,
/// <summary>
/// The controller D-pad down icon.
/// </summary>
ControllerDPadDown = 2,
/// <summary>
/// The controller D-pad left icon.
/// </summary>
ControllerDPadLeft = 3,
/// <summary>
/// The controller D-pad right icon.
/// </summary>
ControllerDPadRight = 4,
/// <summary>
/// The controller D-pad up/down icon.
/// </summary>
ControllerDPadUpDown = 5,
/// <summary>
/// The controller D-pad left/right icon.
/// </summary>
ControllerDPadLeftRight = 6,
/// <summary>
/// The controller D-pad all directions icon.
/// </summary>
ControllerDPadAll = 7,
/// <summary>
/// The controller button 0 icon (Xbox: B, PlayStation: Circle).
/// </summary>
ControllerButton0 = 8,
/// <summary>
/// The controller button 1 icon (XBox: A, PlayStation: Cross).
/// </summary>
ControllerButton1 = 9,
/// <summary>
/// The controller button 2 icon (XBox: X, PlayStation: Square).
/// </summary>
ControllerButton2 = 10,
/// <summary>
/// The controller button 3 icon (BBox: Y, PlayStation: Triangle).
/// </summary>
ControllerButton3 = 11,
/// <summary>
/// The controller left shoulder button icon.
/// </summary>
ControllerShoulderLeft = 12,
/// <summary>
/// The controller right shoulder button icon.
/// </summary>
ControllerShoulderRight = 13,
/// <summary>
/// The controller left trigger button icon.
/// </summary>
ControllerTriggerLeft = 14,
/// <summary>
/// The controller right trigger button icon.
/// </summary>
ControllerTriggerRight = 15,
/// <summary>
/// The controller left analog stick in icon.
/// </summary>
ControllerAnalogLeftStickIn = 16,
/// <summary>
/// The controller right analog stick in icon.
/// </summary>
ControllerAnalogRightStickIn = 17,
/// <summary>
/// The controller start button icon.
/// </summary>
ControllerStart = 18,
/// <summary>
/// The controller back button icon.
/// </summary>
ControllerBack = 19,
/// <summary>
/// The controller left analog stick icon.
/// </summary>
ControllerAnalogLeftStick = 20,
/// <summary>
/// The controller left analog stick up/down icon.
/// </summary>
ControllerAnalogLeftStickUpDown = 21,
/// <summary>
/// The controller left analog stick left/right icon.
/// </summary>
ControllerAnalogLeftStickLeftRight = 22,
/// <summary>
/// The controller right analog stick icon.
/// </summary>
ControllerAnalogRightStick = 23,
/// <summary>
/// The controller right analog stick up/down icon.
/// </summary>
ControllerAnalogRightStickUpDown = 24,
/// <summary>
/// The controller right analog stick left/right icon.
/// </summary>
ControllerAnalogRightStickLeftRight = 25,
/// <summary>
/// The La Noscea region icon.
/// </summary>
LaNoscea = 51,
/// <summary>
/// The Black Shroud region icon.
/// </summary>
BlackShroud = 52,
/// <summary>
/// The Thanalan region icon.
/// </summary>
Thanalan = 53,
/// <summary>
/// The auto translate begin icon.
/// </summary>
AutoTranslateBegin = 54,
/// <summary>
/// The auto translate end icon.
/// </summary>
AutoTranslateEnd = 55,
/// <summary>
/// The fire element icon.
/// </summary>
ElementFire = 56,
/// <summary>
/// The ice element icon.
/// </summary>
ElementIce = 57,
/// <summary>
/// The wind element icon.
/// </summary>
ElementWind = 58,
/// <summary>
/// The earth element icon.
/// </summary>
ElementEarth = 59,
/// <summary>
/// The lightning element icon.
/// </summary>
ElementLightning = 60,
/// <summary>
/// The water element icon.
/// </summary>
ElementWater = 61,
/// <summary>
/// The level sync icon.
/// </summary>
LevelSync = 62,
/// <summary>
/// The warning icon.
/// </summary>
Warning = 63,
/// <summary>
/// The Ishgard region icon.
/// </summary>
Ishgard = 64,
/// <summary>
/// The Aetheryte icon.
/// </summary>
Aetheryte = 65,
/// <summary>
/// The Aethernet icon.
/// </summary>
Aethernet = 66,
/// <summary>
/// The gold star icon.
/// </summary>
GoldStar = 67,
/// <summary>
/// The silver star icon.
/// </summary>
SilverStar = 68,
/// <summary>
/// The green dot icon.
/// </summary>
GreenDot = 70,
/// <summary>
/// The unsheathed sword icon.
/// </summary>
SwordUnsheathed = 71,
/// <summary>
/// The sheathed sword icon.
/// </summary>
SwordSheathed = 72,
/// <summary>
/// The dice icon.
/// </summary>
Dice = 73,
/// <summary>
/// The flyable zone icon.
/// </summary>
FlyZone = 74,
/// <summary>
/// The no-flying zone icon.
/// </summary>
FlyZoneLocked = 75,
/// <summary>
/// The no-circle/prohibited icon.
/// </summary>
NoCircle = 76,
/// <summary>
/// The sprout icon.
/// </summary>
NewAdventurer = 77,
/// <summary>
/// The mentor icon.
/// </summary>
Mentor = 78,
/// <summary>
/// The PvE mentor icon.
/// </summary>
MentorPvE = 79,
/// <summary>
/// The crafting mentor icon.
/// </summary>
MentorCrafting = 80,
/// <summary>
/// The PvP mentor icon.
/// </summary>
MentorPvP = 81,
/// <summary>
/// The tank role icon.
/// </summary>
Tank = 82,
/// <summary>
/// The healer role icon.
/// </summary>
Healer = 83,
/// <summary>
/// The DPS role icon.
/// </summary>
DPS = 84,
/// <summary>
/// The crafter role icon.
/// </summary>
Crafter = 85,
/// <summary>
/// The gatherer role icon.
/// </summary>
Gatherer = 86,
/// <summary>
/// The "any" role icon.
/// </summary>
AnyClass = 87,
/// <summary>
/// The cross-world icon.
/// </summary>
CrossWorld = 88,
/// <summary>
/// The slay type Fate icon.
/// </summary>
FateSlay = 89,
/// <summary>
/// The boss type Fate icon.
/// </summary>
FateBoss = 90,
/// <summary>
/// The gather type Fate icon.
/// </summary>
FateGather = 91,
/// <summary>
/// The defend type Fate icon.
/// </summary>
FateDefend = 92,
/// <summary>
/// The escort type Fate icon.
/// </summary>
FateEscort = 93,
/// <summary>
/// The special type 1 Fate icon.
/// </summary>
FateSpecial1 = 94,
/// <summary>
/// The returner icon.
/// </summary>
Returner = 95,
/// <summary>
/// The Far-East region icon.
/// </summary>
FarEast = 96,
/// <summary>
/// The Gyr Albania region icon.
/// </summary>
GyrAbania = 97,
/// <summary>
/// The special type 2 Fate icon.
/// </summary>
FateSpecial2 = 98,
/// <summary>
/// The priority world icon.
/// </summary>
PriorityWorld = 99,
/// <summary>
/// The elemental level icon.
/// </summary>
ElementalLevel = 100,
/// <summary>
/// The exclamation rectangle icon.
/// </summary>
ExclamationRectangle = 101,
/// <summary>
/// The notorious monster icon.
/// </summary>
NotoriousMonster = 102,
/// <summary>
/// The recording icon.
/// </summary>
Recording = 103,
/// <summary>
/// The alarm icon.
/// </summary>
Alarm = 104,
/// <summary>
/// The arrow up icon.
/// </summary>
ArrowUp = 105,
/// <summary>
/// The arrow down icon.
/// </summary>
ArrowDown = 106,
/// <summary>
/// The Crystarium region icon.
/// </summary>
Crystarium = 107,
/// <summary>
/// The mentor problem icon.
/// </summary>
MentorProblem = 108,
/// <summary>
/// The unknown gold type Fate icon.
/// </summary>
FateUnknownGold = 109,
/// <summary>
/// The orange diamond icon.
/// </summary>
OrangeDiamond = 110,
/// <summary>
/// The crafting type Fate icon.
/// </summary>
FateCrafting = 111,
}
}

View file

@ -1,12 +1,13 @@
namespace Dalamud.Game.Text.SeStringHandling;
/// <summary>
/// An interface binding for a payload that can provide readable Text.
/// </summary>
public interface ITextProvider
namespace Dalamud.Game.Text.SeStringHandling
{
/// <summary>
/// Gets the readable text.
/// An interface binding for a payload that can provide readable Text.
/// </summary>
string Text { get; }
public interface ITextProvider
{
/// <summary>
/// Gets the readable text.
/// </summary>
string Text { get; }
}
}

View file

@ -15,404 +15,405 @@ using Serilog;
// - [SeString] some way to add surrounding formatting information as flags/data to text (or other?) payloads?
// eg, if a text payload is surrounded by italics payloads, strip them out and mark the text payload as italicized
namespace Dalamud.Game.Text.SeStringHandling;
/// <summary>
/// This class represents a parsed SeString payload.
/// </summary>
public abstract partial class Payload
{
// private for now, since subclasses shouldn't interact with this.
// To force-invalidate it, Dirty can be set to true
private byte[] encodedData;
/// <summary>
/// Gets the Lumina instance to use for any necessary data lookups.
/// </summary>
public DataManager DataResolver => Service<DataManager>.Get();
/// <summary>
/// Gets the type of this payload.
/// </summary>
public abstract PayloadType Type { get; }
/// <summary>
/// Gets or sets a value indicating whether whether this payload has been modified since the last Encode().
/// </summary>
public bool Dirty { get; protected set; } = true;
/// <summary>
/// Decodes a binary representation of a payload into its corresponding nice object payload.
/// </summary>
/// <param name="reader">A reader positioned at the start of the payload, and containing at least one entire payload.</param>
/// <returns>The constructed Payload-derived object that was decoded from the binary data.</returns>
public static Payload Decode(BinaryReader reader)
{
var payloadStartPos = reader.BaseStream.Position;
Payload payload;
var initialByte = reader.ReadByte();
reader.BaseStream.Position--;
if (initialByte != START_BYTE)
{
payload = DecodeText(reader);
}
else
{
payload = DecodeChunk(reader);
}
// for now, cache off the actual binary data for this payload, so we don't have to
// regenerate it if the payload isn't modified
// TODO: probably better ways to handle this
var payloadEndPos = reader.BaseStream.Position;
reader.BaseStream.Position = payloadStartPos;
payload.encodedData = reader.ReadBytes((int)(payloadEndPos - payloadStartPos));
payload.Dirty = false;
// Log.Verbose($"got payload bytes {BitConverter.ToString(payload.encodedData).Replace("-", " ")}");
reader.BaseStream.Position = payloadEndPos;
return payload;
}
/// <summary>
/// Encode this payload object into a byte[] useable in-game for things like the chat log.
/// </summary>
/// <param name="force">If true, ignores any cached value and forcibly reencodes the payload from its internal representation.</param>
/// <returns>A byte[] suitable for use with in-game handlers such as the chat log.</returns>
public byte[] Encode(bool force = false)
{
if (this.Dirty || force)
{
this.encodedData = this.EncodeImpl();
this.Dirty = false;
}
return this.encodedData;
}
/// <summary>
/// Encodes the internal state of this payload into a byte[] suitable for sending to in-game
/// handlers such as the chat log.
/// </summary>
/// <returns>Encoded binary payload data suitable for use with in-game handlers.</returns>
protected abstract byte[] EncodeImpl();
/// <summary>
/// Decodes a byte stream from the game into a payload object.
/// </summary>
/// <param name="reader">A BinaryReader containing at least all the data for this payload.</param>
/// <param name="endOfStream">The location holding the end of the data for this payload.</param>
// TODO: endOfStream is somewhat legacy now that payload length is always handled correctly.
// This could be changed to just take a straight byte[], but that would complicate reading
// but we could probably at least remove the end param
protected abstract void DecodeImpl(BinaryReader reader, long endOfStream);
private static Payload DecodeChunk(BinaryReader reader)
{
Payload payload = null;
reader.ReadByte(); // START_BYTE
var chunkType = (SeStringChunkType)reader.ReadByte();
var chunkLen = GetInteger(reader);
var packetStart = reader.BaseStream.Position;
// any unhandled payload types will be turned into a RawPayload with the exact same binary data
switch (chunkType)
{
case SeStringChunkType.EmphasisItalic:
payload = new EmphasisItalicPayload();
break;
case SeStringChunkType.NewLine:
payload = NewLinePayload.Payload;
break;
case SeStringChunkType.SeHyphen:
payload = SeHyphenPayload.Payload;
break;
case SeStringChunkType.Interactable:
{
var subType = (EmbeddedInfoType)reader.ReadByte();
switch (subType)
{
case EmbeddedInfoType.PlayerName:
payload = new PlayerPayload();
break;
case EmbeddedInfoType.ItemLink:
payload = new ItemPayload();
break;
case EmbeddedInfoType.MapPositionLink:
payload = new MapLinkPayload();
break;
case EmbeddedInfoType.Status:
payload = new StatusPayload();
break;
case EmbeddedInfoType.QuestLink:
payload = new QuestPayload();
break;
case EmbeddedInfoType.DalamudLink:
payload = new DalamudLinkPayload();
break;
case EmbeddedInfoType.LinkTerminator:
// this has no custom handling and so needs to fallthrough to ensure it is captured
default:
// but I'm also tired of this log
if (subType != EmbeddedInfoType.LinkTerminator)
{
Log.Verbose("Unhandled EmbeddedInfoType: {0}", subType);
}
// rewind so we capture the Interactable byte in the raw data
reader.BaseStream.Seek(-1, SeekOrigin.Current);
break;
}
}
break;
case SeStringChunkType.AutoTranslateKey:
payload = new AutoTranslatePayload();
break;
case SeStringChunkType.UIForeground:
payload = new UIForegroundPayload();
break;
case SeStringChunkType.UIGlow:
payload = new UIGlowPayload();
break;
case SeStringChunkType.Icon:
payload = new IconPayload();
break;
default:
Log.Verbose("Unhandled SeStringChunkType: {0}", chunkType);
break;
}
payload ??= new RawPayload((byte)chunkType);
payload.DecodeImpl(reader, reader.BaseStream.Position + chunkLen - 1);
// read through the rest of the packet
var readBytes = (uint)(reader.BaseStream.Position - packetStart);
reader.ReadBytes((int)(chunkLen - readBytes + 1)); // +1 for the END_BYTE marker
return payload;
}
private static Payload DecodeText(BinaryReader reader)
{
var payload = new TextPayload();
payload.DecodeImpl(reader, reader.BaseStream.Length);
return payload;
}
}
/// <summary>
/// Parsing helpers.
/// </summary>
public abstract partial class Payload
namespace Dalamud.Game.Text.SeStringHandling
{
/// <summary>
/// The start byte of a payload.
/// This class represents a parsed SeString payload.
/// </summary>
[SuppressMessage("StyleCop.CSharp.NamingRules", "SA1310:Field names should not contain underscore", Justification = "This is prefered.")]
protected const byte START_BYTE = 0x02;
/// <summary>
/// The end byte of a payload.
/// </summary>
[SuppressMessage("StyleCop.CSharp.NamingRules", "SA1310:Field names should not contain underscore", Justification = "This is prefered.")]
protected const byte END_BYTE = 0x03;
/// <summary>
/// This represents the type of embedded info in a payload.
/// </summary>
public enum EmbeddedInfoType
public abstract partial class Payload
{
/// <summary>
/// A player's name.
/// </summary>
PlayerName = 0x01,
// private for now, since subclasses shouldn't interact with this.
// To force-invalidate it, Dirty can be set to true
private byte[] encodedData;
/// <summary>
/// The link to an iteme.
/// Gets the Lumina instance to use for any necessary data lookups.
/// </summary>
ItemLink = 0x03,
public DataManager DataResolver => Service<DataManager>.Get();
/// <summary>
/// The link to a map position.
/// Gets the type of this payload.
/// </summary>
MapPositionLink = 0x04,
public abstract PayloadType Type { get; }
/// <summary>
/// The link to a quest.
/// Gets or sets a value indicating whether whether this payload has been modified since the last Encode().
/// </summary>
QuestLink = 0x05,
public bool Dirty { get; protected set; } = true;
/// <summary>
/// A status effect.
/// Decodes a binary representation of a payload into its corresponding nice object payload.
/// </summary>
Status = 0x09,
/// <summary>
/// A custom Dalamud link.
/// </summary>
DalamudLink = 0x0F,
/// <summary>
/// A link terminator.
/// </summary>
/// <remarks>
/// It is not exactly clear what this is, but seems to always follow a link.
/// </remarks>
LinkTerminator = 0xCF,
}
/// <summary>
/// This represents the type of payload and how it should be encoded.
/// </summary>
protected enum SeStringChunkType
{
/// <summary>
/// See the <see cref="IconPayload"/> class.
/// </summary>
Icon = 0x12,
/// <summary>
/// See the <see cref="EmphasisItalicPayload"/> class.
/// </summary>
EmphasisItalic = 0x1A,
/// <summary>
/// See the <see cref="NewLinePayload"/>.
/// </summary>
NewLine = 0x10,
/// <summary>
/// See the <see cref="SeHyphenPayload"/> class.
/// </summary>
SeHyphen = 0x1F,
/// <summary>
/// See any of the link-type classes:
/// <see cref="PlayerPayload"/>,
/// <see cref="ItemPayload"/>,
/// <see cref="MapLinkPayload"/>,
/// <see cref="StatusPayload"/>,
/// <see cref="QuestPayload"/>,
/// <see cref="DalamudLinkPayload"/>.
/// </summary>
Interactable = 0x27,
/// <summary>
/// See the <see cref="AutoTranslatePayload"/> class.
/// </summary>
AutoTranslateKey = 0x2E,
/// <summary>
/// See the <see cref="UIForegroundPayload"/> class.
/// </summary>
UIForeground = 0x48,
/// <summary>
/// See the <see cref="UIGlowPayload"/> class.
/// </summary>
UIGlow = 0x49,
}
/// <summary>
/// Retrieve the packed integer from SE's native data format.
/// </summary>
/// <param name="input">The BinaryReader instance.</param>
/// <returns>An integer.</returns>
// made protected, unless we actually want to use it externally
// in which case it should probably go live somewhere else
protected static uint GetInteger(BinaryReader input)
{
uint marker = input.ReadByte();
if (marker < 0xD0)
return marker - 1;
// the game adds 0xF0 marker for values >= 0xCF
// uasge of 0xD0-0xEF is unknown, should we throw here?
// if (marker < 0xF0) throw new NotSupportedException();
marker = (marker + 1) & 0b1111;
var ret = new byte[4];
for (var i = 3; i >= 0; i--)
/// <param name="reader">A reader positioned at the start of the payload, and containing at least one entire payload.</param>
/// <returns>The constructed Payload-derived object that was decoded from the binary data.</returns>
public static Payload Decode(BinaryReader reader)
{
ret[i] = (marker & (1 << i)) == 0 ? (byte)0 : input.ReadByte();
}
var payloadStartPos = reader.BaseStream.Position;
return BitConverter.ToUInt32(ret, 0);
}
Payload payload;
/// <summary>
/// Create a packed integer in Se's native data format.
/// </summary>
/// <param name="value">The value to pack.</param>
/// <returns>A packed integer.</returns>
protected static byte[] MakeInteger(uint value)
{
if (value < 0xCF)
{
return new byte[] { (byte)(value + 1) };
}
var bytes = BitConverter.GetBytes(value);
var ret = new List<byte>() { 0xF0 };
for (var i = 3; i >= 0; i--)
{
if (bytes[i] != 0)
var initialByte = reader.ReadByte();
reader.BaseStream.Position--;
if (initialByte != START_BYTE)
{
ret.Add(bytes[i]);
ret[0] |= (byte)(1 << i);
payload = DecodeText(reader);
}
else
{
payload = DecodeChunk(reader);
}
// for now, cache off the actual binary data for this payload, so we don't have to
// regenerate it if the payload isn't modified
// TODO: probably better ways to handle this
var payloadEndPos = reader.BaseStream.Position;
reader.BaseStream.Position = payloadStartPos;
payload.encodedData = reader.ReadBytes((int)(payloadEndPos - payloadStartPos));
payload.Dirty = false;
// Log.Verbose($"got payload bytes {BitConverter.ToString(payload.encodedData).Replace("-", " ")}");
reader.BaseStream.Position = payloadEndPos;
return payload;
}
ret[0] -= 1;
/// <summary>
/// Encode this payload object into a byte[] useable in-game for things like the chat log.
/// </summary>
/// <param name="force">If true, ignores any cached value and forcibly reencodes the payload from its internal representation.</param>
/// <returns>A byte[] suitable for use with in-game handlers such as the chat log.</returns>
public byte[] Encode(bool force = false)
{
if (this.Dirty || force)
{
this.encodedData = this.EncodeImpl();
this.Dirty = false;
}
return ret.ToArray();
return this.encodedData;
}
/// <summary>
/// Encodes the internal state of this payload into a byte[] suitable for sending to in-game
/// handlers such as the chat log.
/// </summary>
/// <returns>Encoded binary payload data suitable for use with in-game handlers.</returns>
protected abstract byte[] EncodeImpl();
/// <summary>
/// Decodes a byte stream from the game into a payload object.
/// </summary>
/// <param name="reader">A BinaryReader containing at least all the data for this payload.</param>
/// <param name="endOfStream">The location holding the end of the data for this payload.</param>
// TODO: endOfStream is somewhat legacy now that payload length is always handled correctly.
// This could be changed to just take a straight byte[], but that would complicate reading
// but we could probably at least remove the end param
protected abstract void DecodeImpl(BinaryReader reader, long endOfStream);
private static Payload DecodeChunk(BinaryReader reader)
{
Payload payload = null;
reader.ReadByte(); // START_BYTE
var chunkType = (SeStringChunkType)reader.ReadByte();
var chunkLen = GetInteger(reader);
var packetStart = reader.BaseStream.Position;
// any unhandled payload types will be turned into a RawPayload with the exact same binary data
switch (chunkType)
{
case SeStringChunkType.EmphasisItalic:
payload = new EmphasisItalicPayload();
break;
case SeStringChunkType.NewLine:
payload = NewLinePayload.Payload;
break;
case SeStringChunkType.SeHyphen:
payload = SeHyphenPayload.Payload;
break;
case SeStringChunkType.Interactable:
{
var subType = (EmbeddedInfoType)reader.ReadByte();
switch (subType)
{
case EmbeddedInfoType.PlayerName:
payload = new PlayerPayload();
break;
case EmbeddedInfoType.ItemLink:
payload = new ItemPayload();
break;
case EmbeddedInfoType.MapPositionLink:
payload = new MapLinkPayload();
break;
case EmbeddedInfoType.Status:
payload = new StatusPayload();
break;
case EmbeddedInfoType.QuestLink:
payload = new QuestPayload();
break;
case EmbeddedInfoType.DalamudLink:
payload = new DalamudLinkPayload();
break;
case EmbeddedInfoType.LinkTerminator:
// this has no custom handling and so needs to fallthrough to ensure it is captured
default:
// but I'm also tired of this log
if (subType != EmbeddedInfoType.LinkTerminator)
{
Log.Verbose("Unhandled EmbeddedInfoType: {0}", subType);
}
// rewind so we capture the Interactable byte in the raw data
reader.BaseStream.Seek(-1, SeekOrigin.Current);
break;
}
}
break;
case SeStringChunkType.AutoTranslateKey:
payload = new AutoTranslatePayload();
break;
case SeStringChunkType.UIForeground:
payload = new UIForegroundPayload();
break;
case SeStringChunkType.UIGlow:
payload = new UIGlowPayload();
break;
case SeStringChunkType.Icon:
payload = new IconPayload();
break;
default:
Log.Verbose("Unhandled SeStringChunkType: {0}", chunkType);
break;
}
payload ??= new RawPayload((byte)chunkType);
payload.DecodeImpl(reader, reader.BaseStream.Position + chunkLen - 1);
// read through the rest of the packet
var readBytes = (uint)(reader.BaseStream.Position - packetStart);
reader.ReadBytes((int)(chunkLen - readBytes + 1)); // +1 for the END_BYTE marker
return payload;
}
private static Payload DecodeText(BinaryReader reader)
{
var payload = new TextPayload();
payload.DecodeImpl(reader, reader.BaseStream.Length);
return payload;
}
}
/// <summary>
/// From a binary packed integer, get the high and low bytes.
/// Parsing helpers.
/// </summary>
/// <param name="input">The BinaryReader instance.</param>
/// <returns>The high and low bytes.</returns>
protected static (uint High, uint Low) GetPackedIntegers(BinaryReader input)
public abstract partial class Payload
{
var value = GetInteger(input);
return (value >> 16, value & 0xFFFF);
}
/// <summary>
/// The start byte of a payload.
/// </summary>
[SuppressMessage("StyleCop.CSharp.NamingRules", "SA1310:Field names should not contain underscore", Justification = "This is prefered.")]
protected const byte START_BYTE = 0x02;
/// <summary>
/// Create a packed integer from the given high and low bytes.
/// </summary>
/// <param name="high">The high order bytes.</param>
/// <param name="low">The low order bytes.</param>
/// <returns>A packed integer.</returns>
protected static byte[] MakePackedInteger(uint high, uint low)
{
var value = (high << 16) | (low & 0xFFFF);
return MakeInteger(value);
/// <summary>
/// The end byte of a payload.
/// </summary>
[SuppressMessage("StyleCop.CSharp.NamingRules", "SA1310:Field names should not contain underscore", Justification = "This is prefered.")]
protected const byte END_BYTE = 0x03;
/// <summary>
/// This represents the type of embedded info in a payload.
/// </summary>
public enum EmbeddedInfoType
{
/// <summary>
/// A player's name.
/// </summary>
PlayerName = 0x01,
/// <summary>
/// The link to an iteme.
/// </summary>
ItemLink = 0x03,
/// <summary>
/// The link to a map position.
/// </summary>
MapPositionLink = 0x04,
/// <summary>
/// The link to a quest.
/// </summary>
QuestLink = 0x05,
/// <summary>
/// A status effect.
/// </summary>
Status = 0x09,
/// <summary>
/// A custom Dalamud link.
/// </summary>
DalamudLink = 0x0F,
/// <summary>
/// A link terminator.
/// </summary>
/// <remarks>
/// It is not exactly clear what this is, but seems to always follow a link.
/// </remarks>
LinkTerminator = 0xCF,
}
/// <summary>
/// This represents the type of payload and how it should be encoded.
/// </summary>
protected enum SeStringChunkType
{
/// <summary>
/// See the <see cref="IconPayload"/> class.
/// </summary>
Icon = 0x12,
/// <summary>
/// See the <see cref="EmphasisItalicPayload"/> class.
/// </summary>
EmphasisItalic = 0x1A,
/// <summary>
/// See the <see cref="NewLinePayload"/>.
/// </summary>
NewLine = 0x10,
/// <summary>
/// See the <see cref="SeHyphenPayload"/> class.
/// </summary>
SeHyphen = 0x1F,
/// <summary>
/// See any of the link-type classes:
/// <see cref="PlayerPayload"/>,
/// <see cref="ItemPayload"/>,
/// <see cref="MapLinkPayload"/>,
/// <see cref="StatusPayload"/>,
/// <see cref="QuestPayload"/>,
/// <see cref="DalamudLinkPayload"/>.
/// </summary>
Interactable = 0x27,
/// <summary>
/// See the <see cref="AutoTranslatePayload"/> class.
/// </summary>
AutoTranslateKey = 0x2E,
/// <summary>
/// See the <see cref="UIForegroundPayload"/> class.
/// </summary>
UIForeground = 0x48,
/// <summary>
/// See the <see cref="UIGlowPayload"/> class.
/// </summary>
UIGlow = 0x49,
}
/// <summary>
/// Retrieve the packed integer from SE's native data format.
/// </summary>
/// <param name="input">The BinaryReader instance.</param>
/// <returns>An integer.</returns>
// made protected, unless we actually want to use it externally
// in which case it should probably go live somewhere else
protected static uint GetInteger(BinaryReader input)
{
uint marker = input.ReadByte();
if (marker < 0xD0)
return marker - 1;
// the game adds 0xF0 marker for values >= 0xCF
// uasge of 0xD0-0xEF is unknown, should we throw here?
// if (marker < 0xF0) throw new NotSupportedException();
marker = (marker + 1) & 0b1111;
var ret = new byte[4];
for (var i = 3; i >= 0; i--)
{
ret[i] = (marker & (1 << i)) == 0 ? (byte)0 : input.ReadByte();
}
return BitConverter.ToUInt32(ret, 0);
}
/// <summary>
/// Create a packed integer in Se's native data format.
/// </summary>
/// <param name="value">The value to pack.</param>
/// <returns>A packed integer.</returns>
protected static byte[] MakeInteger(uint value)
{
if (value < 0xCF)
{
return new byte[] { (byte)(value + 1) };
}
var bytes = BitConverter.GetBytes(value);
var ret = new List<byte>() { 0xF0 };
for (var i = 3; i >= 0; i--)
{
if (bytes[i] != 0)
{
ret.Add(bytes[i]);
ret[0] |= (byte)(1 << i);
}
}
ret[0] -= 1;
return ret.ToArray();
}
/// <summary>
/// From a binary packed integer, get the high and low bytes.
/// </summary>
/// <param name="input">The BinaryReader instance.</param>
/// <returns>The high and low bytes.</returns>
protected static (uint High, uint Low) GetPackedIntegers(BinaryReader input)
{
var value = GetInteger(input);
return (value >> 16, value & 0xFFFF);
}
/// <summary>
/// Create a packed integer from the given high and low bytes.
/// </summary>
/// <param name="high">The high order bytes.</param>
/// <param name="low">The low order bytes.</param>
/// <returns>A packed integer.</returns>
protected static byte[] MakePackedInteger(uint high, uint low)
{
var value = (high << 16) | (low & 0xFFFF);
return MakeInteger(value);
}
}
}

View file

@ -1,82 +1,83 @@
namespace Dalamud.Game.Text.SeStringHandling;
/// <summary>
/// All parsed types of SeString payloads.
/// </summary>
public enum PayloadType
namespace Dalamud.Game.Text.SeStringHandling
{
/// <summary>
/// An unknown SeString.
/// All parsed types of SeString payloads.
/// </summary>
Unknown,
public enum PayloadType
{
/// <summary>
/// An unknown SeString.
/// </summary>
Unknown,
/// <summary>
/// An SeString payload representing a player link.
/// </summary>
Player,
/// <summary>
/// An SeString payload representing a player link.
/// </summary>
Player,
/// <summary>
/// An SeString payload representing an Item link.
/// </summary>
Item,
/// <summary>
/// An SeString payload representing an Item link.
/// </summary>
Item,
/// <summary>
/// An SeString payload representing an Status Effect link.
/// </summary>
Status,
/// <summary>
/// An SeString payload representing an Status Effect link.
/// </summary>
Status,
/// <summary>
/// An SeString payload representing raw, typed text.
/// </summary>
RawText,
/// <summary>
/// An SeString payload representing raw, typed text.
/// </summary>
RawText,
/// <summary>
/// An SeString payload representing a text foreground color.
/// </summary>
UIForeground,
/// <summary>
/// An SeString payload representing a text foreground color.
/// </summary>
UIForeground,
/// <summary>
/// An SeString payload representing a text glow color.
/// </summary>
UIGlow,
/// <summary>
/// An SeString payload representing a text glow color.
/// </summary>
UIGlow,
/// <summary>
/// An SeString payload representing a map position link, such as from &lt;flag&gt; or &lt;pos&gt;.
/// </summary>
MapLink,
/// <summary>
/// An SeString payload representing a map position link, such as from &lt;flag&gt; or &lt;pos&gt;.
/// </summary>
MapLink,
/// <summary>
/// An SeString payload representing an auto-translate dictionary entry.
/// </summary>
AutoTranslateText,
/// <summary>
/// An SeString payload representing an auto-translate dictionary entry.
/// </summary>
AutoTranslateText,
/// <summary>
/// An SeString payload representing italic emphasis formatting on text.
/// </summary>
EmphasisItalic,
/// <summary>
/// An SeString payload representing italic emphasis formatting on text.
/// </summary>
EmphasisItalic,
/// <summary>
/// An SeString payload representing a bitmap icon.
/// </summary>
Icon,
/// <summary>
/// An SeString payload representing a bitmap icon.
/// </summary>
Icon,
/// <summary>
/// A SeString payload representing a quest link.
/// </summary>
Quest,
/// <summary>
/// A SeString payload representing a quest link.
/// </summary>
Quest,
/// <summary>
/// A SeString payload representing a custom clickable link for dalamud plugins.
/// </summary>
DalamudLink,
/// <summary>
/// A SeString payload representing a custom clickable link for dalamud plugins.
/// </summary>
DalamudLink,
/// <summary>
/// An SeString payload representing a newline character.
/// </summary>
NewLine,
/// <summary>
/// An SeString payload representing a newline character.
/// </summary>
NewLine,
/// <summary>
/// An SeString payload representing a doublewide SE hypen.
/// </summary>
SeHyphen,
/// <summary>
/// An SeString payload representing a doublewide SE hypen.
/// </summary>
SeHyphen,
}
}

View file

@ -7,164 +7,165 @@ using Lumina.Excel.GeneratedSheets;
using Newtonsoft.Json;
using Serilog;
namespace Dalamud.Game.Text.SeStringHandling.Payloads;
/// <summary>
/// An SeString Payload containing an auto-translation/completion chat message.
/// </summary>
public class AutoTranslatePayload : Payload, ITextProvider
namespace Dalamud.Game.Text.SeStringHandling.Payloads
{
private string text;
[JsonProperty]
private uint group;
[JsonProperty]
private uint key;
/// <summary>
/// Initializes a new instance of the <see cref="AutoTranslatePayload"/> class.
/// Creates a new auto-translate payload.
/// An SeString Payload containing an auto-translation/completion chat message.
/// </summary>
/// <param name="group">The group id for this message.</param>
/// <param name="key">The key/row id for this message. Which table this is in depends on the group id and details the Completion table.</param>
/// <remarks>
/// This table is somewhat complicated in structure, and so using this constructor may not be very nice.
/// There is probably little use to create one of these, however.
/// </remarks>
public AutoTranslatePayload(uint group, uint key)
public class AutoTranslatePayload : Payload, ITextProvider
{
// TODO: friendlier ctor? not sure how to handle that given how weird the tables are
this.group = group;
this.key = key;
}
private string text;
/// <summary>
/// Initializes a new instance of the <see cref="AutoTranslatePayload"/> class.
/// </summary>
internal AutoTranslatePayload()
{
}
[JsonProperty]
private uint group;
/// <inheritdoc/>
public override PayloadType Type => PayloadType.AutoTranslateText;
[JsonProperty]
private uint key;
/// <summary>
/// Gets the actual text displayed in-game for this payload.
/// </summary>
/// <remarks>
/// Value is evaluated lazily and cached.
/// </remarks>
public string Text
{
get
/// <summary>
/// Initializes a new instance of the <see cref="AutoTranslatePayload"/> class.
/// Creates a new auto-translate payload.
/// </summary>
/// <param name="group">The group id for this message.</param>
/// <param name="key">The key/row id for this message. Which table this is in depends on the group id and details the Completion table.</param>
/// <remarks>
/// This table is somewhat complicated in structure, and so using this constructor may not be very nice.
/// There is probably little use to create one of these, however.
/// </remarks>
public AutoTranslatePayload(uint group, uint key)
{
// wrap the text in the colored brackets that is uses in-game, since those are not actually part of any of the payloads
return this.text ??= $"{(char)SeIconChar.AutoTranslateOpen} {this.Resolve()} {(char)SeIconChar.AutoTranslateClose}";
// TODO: friendlier ctor? not sure how to handle that given how weird the tables are
this.group = group;
this.key = key;
}
}
/// <summary>
/// Returns a string that represents the current object.
/// </summary>
/// <returns>A string that represents the current object.</returns>
public override string ToString()
{
return $"{this.Type} - Group: {this.group}, Key: {this.key}, Text: {this.Text}";
}
/// <summary>
/// Initializes a new instance of the <see cref="AutoTranslatePayload"/> class.
/// </summary>
internal AutoTranslatePayload()
{
}
/// <inheritdoc/>
protected override byte[] EncodeImpl()
{
var keyBytes = MakeInteger(this.key);
/// <inheritdoc/>
public override PayloadType Type => PayloadType.AutoTranslateText;
var chunkLen = keyBytes.Length + 2;
var bytes = new List<byte>()
/// <summary>
/// Gets the actual text displayed in-game for this payload.
/// </summary>
/// <remarks>
/// Value is evaluated lazily and cached.
/// </remarks>
public string Text
{
get
{
// wrap the text in the colored brackets that is uses in-game, since those are not actually part of any of the payloads
return this.text ??= $"{(char)SeIconChar.AutoTranslateOpen} {this.Resolve()} {(char)SeIconChar.AutoTranslateClose}";
}
}
/// <summary>
/// Returns a string that represents the current object.
/// </summary>
/// <returns>A string that represents the current object.</returns>
public override string ToString()
{
return $"{this.Type} - Group: {this.group}, Key: {this.key}, Text: {this.Text}";
}
/// <inheritdoc/>
protected override byte[] EncodeImpl()
{
var keyBytes = MakeInteger(this.key);
var chunkLen = keyBytes.Length + 2;
var bytes = new List<byte>()
{
START_BYTE,
(byte)SeStringChunkType.AutoTranslateKey, (byte)chunkLen,
(byte)this.group,
};
bytes.AddRange(keyBytes);
bytes.Add(END_BYTE);
bytes.AddRange(keyBytes);
bytes.Add(END_BYTE);
return bytes.ToArray();
}
/// <inheritdoc/>
protected override void DecodeImpl(BinaryReader reader, long endOfStream)
{
// this seems to always be a bare byte, and not following normal integer encoding
// the values in the table are all <70 so this is presumably ok
this.group = reader.ReadByte();
this.key = GetInteger(reader);
}
private string Resolve()
{
string value = null;
var sheet = this.DataResolver.GetExcelSheet<Completion>();
Completion row = null;
try
{
// try to get the row in the Completion table itself, because this is 'easiest'
// The row may not exist at all (if the Key is for another table), or it could be the wrong row
// (again, if it's meant for another table)
row = sheet.GetRow(this.key);
return bytes.ToArray();
}
catch
{
} // don't care, row will be null
if (row?.Group == this.group)
/// <inheritdoc/>
protected override void DecodeImpl(BinaryReader reader, long endOfStream)
{
// if the row exists in this table and the group matches, this is actually the correct data
value = row.Text;
// this seems to always be a bare byte, and not following normal integer encoding
// the values in the table are all <70 so this is presumably ok
this.group = reader.ReadByte();
this.key = GetInteger(reader);
}
else
private string Resolve()
{
string value = null;
var sheet = this.DataResolver.GetExcelSheet<Completion>();
Completion row = null;
try
{
// we need to get the linked table and do the lookup there instead
// in this case, there will only be one entry for this group id
row = sheet.First(r => r.Group == this.group);
// many of the names contain valid id ranges after the table name, but we don't need those
var actualTableName = row.LookupTable.RawString.Split('[')[0];
var name = actualTableName switch
{
"Action" => this.DataResolver.GetExcelSheet<Lumina.Excel.GeneratedSheets.Action>().GetRow(this.key).Name,
"ActionComboRoute" => this.DataResolver.GetExcelSheet<ActionComboRoute>().GetRow(this.key).Name,
"BuddyAction" => this.DataResolver.GetExcelSheet<BuddyAction>().GetRow(this.key).Name,
"ClassJob" => this.DataResolver.GetExcelSheet<ClassJob>().GetRow(this.key).Name,
"Companion" => this.DataResolver.GetExcelSheet<Companion>().GetRow(this.key).Singular,
"CraftAction" => this.DataResolver.GetExcelSheet<CraftAction>().GetRow(this.key).Name,
"GeneralAction" => this.DataResolver.GetExcelSheet<GeneralAction>().GetRow(this.key).Name,
"GuardianDeity" => this.DataResolver.GetExcelSheet<GuardianDeity>().GetRow(this.key).Name,
"MainCommand" => this.DataResolver.GetExcelSheet<MainCommand>().GetRow(this.key).Name,
"Mount" => this.DataResolver.GetExcelSheet<Mount>().GetRow(this.key).Singular,
"Pet" => this.DataResolver.GetExcelSheet<Pet>().GetRow(this.key).Name,
"PetAction" => this.DataResolver.GetExcelSheet<PetAction>().GetRow(this.key).Name,
"PetMirage" => this.DataResolver.GetExcelSheet<PetMirage>().GetRow(this.key).Name,
"PlaceName" => this.DataResolver.GetExcelSheet<PlaceName>().GetRow(this.key).Name,
"Race" => this.DataResolver.GetExcelSheet<Race>().GetRow(this.key).Masculine,
"TextCommand" => this.DataResolver.GetExcelSheet<TextCommand>().GetRow(this.key).Command,
"Tribe" => this.DataResolver.GetExcelSheet<Tribe>().GetRow(this.key).Masculine,
"Weather" => this.DataResolver.GetExcelSheet<Weather>().GetRow(this.key).Name,
_ => throw new Exception(actualTableName),
};
value = name;
// try to get the row in the Completion table itself, because this is 'easiest'
// The row may not exist at all (if the Key is for another table), or it could be the wrong row
// (again, if it's meant for another table)
row = sheet.GetRow(this.key);
}
catch (Exception e)
catch
{
Log.Error(e, $"AutoTranslatePayload - failed to resolve: {this.Type} - Group: {this.group}, Key: {this.key}");
}
}
} // don't care, row will be null
return value;
if (row?.Group == this.group)
{
// if the row exists in this table and the group matches, this is actually the correct data
value = row.Text;
}
else
{
try
{
// we need to get the linked table and do the lookup there instead
// in this case, there will only be one entry for this group id
row = sheet.First(r => r.Group == this.group);
// many of the names contain valid id ranges after the table name, but we don't need those
var actualTableName = row.LookupTable.RawString.Split('[')[0];
var name = actualTableName switch
{
"Action" => this.DataResolver.GetExcelSheet<Lumina.Excel.GeneratedSheets.Action>().GetRow(this.key).Name,
"ActionComboRoute" => this.DataResolver.GetExcelSheet<ActionComboRoute>().GetRow(this.key).Name,
"BuddyAction" => this.DataResolver.GetExcelSheet<BuddyAction>().GetRow(this.key).Name,
"ClassJob" => this.DataResolver.GetExcelSheet<ClassJob>().GetRow(this.key).Name,
"Companion" => this.DataResolver.GetExcelSheet<Companion>().GetRow(this.key).Singular,
"CraftAction" => this.DataResolver.GetExcelSheet<CraftAction>().GetRow(this.key).Name,
"GeneralAction" => this.DataResolver.GetExcelSheet<GeneralAction>().GetRow(this.key).Name,
"GuardianDeity" => this.DataResolver.GetExcelSheet<GuardianDeity>().GetRow(this.key).Name,
"MainCommand" => this.DataResolver.GetExcelSheet<MainCommand>().GetRow(this.key).Name,
"Mount" => this.DataResolver.GetExcelSheet<Mount>().GetRow(this.key).Singular,
"Pet" => this.DataResolver.GetExcelSheet<Pet>().GetRow(this.key).Name,
"PetAction" => this.DataResolver.GetExcelSheet<PetAction>().GetRow(this.key).Name,
"PetMirage" => this.DataResolver.GetExcelSheet<PetMirage>().GetRow(this.key).Name,
"PlaceName" => this.DataResolver.GetExcelSheet<PlaceName>().GetRow(this.key).Name,
"Race" => this.DataResolver.GetExcelSheet<Race>().GetRow(this.key).Masculine,
"TextCommand" => this.DataResolver.GetExcelSheet<TextCommand>().GetRow(this.key).Command,
"Tribe" => this.DataResolver.GetExcelSheet<Tribe>().GetRow(this.key).Masculine,
"Weather" => this.DataResolver.GetExcelSheet<Weather>().GetRow(this.key).Name,
_ => throw new Exception(actualTableName),
};
value = name;
}
catch (Exception e)
{
Log.Error(e, $"AutoTranslatePayload - failed to resolve: {this.Type} - Group: {this.group}, Key: {this.key}");
}
}
return value;
}
}
}

View file

@ -3,56 +3,57 @@ using System.Collections.Generic;
using System.IO;
using System.Text;
namespace Dalamud.Game.Text.SeStringHandling.Payloads;
/// <summary>
/// This class represents a custom Dalamud clickable chat link.
/// </summary>
public class DalamudLinkPayload : Payload
namespace Dalamud.Game.Text.SeStringHandling.Payloads
{
/// <inheritdoc/>
public override PayloadType Type => PayloadType.DalamudLink;
/// <summary>
/// Gets the plugin command ID to be linked.
/// This class represents a custom Dalamud clickable chat link.
/// </summary>
public uint CommandId { get; internal set; } = 0;
/// <summary>
/// Gets the plugin name to be linked.
/// </summary>
public string Plugin { get; internal set; } = string.Empty;
/// <inheritdoc/>
public override string ToString()
public class DalamudLinkPayload : Payload
{
return $"{this.Type} - Plugin: {this.Plugin}, Command: {this.CommandId}";
}
/// <inheritdoc/>
public override PayloadType Type => PayloadType.DalamudLink;
/// <inheritdoc/>
protected override byte[] EncodeImpl()
{
var pluginBytes = Encoding.UTF8.GetBytes(this.Plugin);
var commandBytes = MakeInteger(this.CommandId);
var chunkLen = 3 + pluginBytes.Length + commandBytes.Length;
/// <summary>
/// Gets the plugin command ID to be linked.
/// </summary>
public uint CommandId { get; internal set; } = 0;
if (chunkLen > 255)
/// <summary>
/// Gets the plugin name to be linked.
/// </summary>
public string Plugin { get; internal set; } = string.Empty;
/// <inheritdoc/>
public override string ToString()
{
throw new Exception("Chunk is too long. Plugin name exceeds limits for DalamudLinkPayload");
return $"{this.Type} - Plugin: {this.Plugin}, Command: {this.CommandId}";
}
var bytes = new List<byte> { START_BYTE, (byte)SeStringChunkType.Interactable, (byte)chunkLen, (byte)EmbeddedInfoType.DalamudLink };
bytes.Add((byte)pluginBytes.Length);
bytes.AddRange(pluginBytes);
bytes.AddRange(commandBytes);
bytes.Add(END_BYTE);
return bytes.ToArray();
}
/// <inheritdoc/>
protected override byte[] EncodeImpl()
{
var pluginBytes = Encoding.UTF8.GetBytes(this.Plugin);
var commandBytes = MakeInteger(this.CommandId);
var chunkLen = 3 + pluginBytes.Length + commandBytes.Length;
/// <inheritdoc/>
protected override void DecodeImpl(BinaryReader reader, long endOfStream)
{
this.Plugin = Encoding.UTF8.GetString(reader.ReadBytes(reader.ReadByte()));
this.CommandId = GetInteger(reader);
if (chunkLen > 255)
{
throw new Exception("Chunk is too long. Plugin name exceeds limits for DalamudLinkPayload");
}
var bytes = new List<byte> { START_BYTE, (byte)SeStringChunkType.Interactable, (byte)chunkLen, (byte)EmbeddedInfoType.DalamudLink };
bytes.Add((byte)pluginBytes.Length);
bytes.AddRange(pluginBytes);
bytes.AddRange(commandBytes);
bytes.Add(END_BYTE);
return bytes.ToArray();
}
/// <inheritdoc/>
protected override void DecodeImpl(BinaryReader reader, long endOfStream)
{
this.Plugin = Encoding.UTF8.GetString(reader.ReadBytes(reader.ReadByte()));
this.CommandId = GetInteger(reader);
}
}
}

View file

@ -2,80 +2,81 @@ using System;
using System.Collections.Generic;
using System.IO;
namespace Dalamud.Game.Text.SeStringHandling.Payloads;
/// <summary>
/// An SeString Payload containing information about enabling or disabling italics formatting on following text.
/// </summary>
/// <remarks>
/// As with other formatting payloads, this is only useful in a payload block, where it affects any subsequent
/// text payloads.
/// </remarks>
public class EmphasisItalicPayload : Payload
namespace Dalamud.Game.Text.SeStringHandling.Payloads
{
/// <summary>
/// Initializes a new instance of the <see cref="EmphasisItalicPayload"/> class.
/// Creates an EmphasisItalicPayload.
/// An SeString Payload containing information about enabling or disabling italics formatting on following text.
/// </summary>
/// <param name="enabled">Whether italics formatting should be enabled or disabled for following text.</param>
public EmphasisItalicPayload(bool enabled)
/// <remarks>
/// As with other formatting payloads, this is only useful in a payload block, where it affects any subsequent
/// text payloads.
/// </remarks>
public class EmphasisItalicPayload : Payload
{
this.IsEnabled = enabled;
}
/// <summary>
/// Initializes a new instance of the <see cref="EmphasisItalicPayload"/> class.
/// Creates an EmphasisItalicPayload.
/// </summary>
/// <param name="enabled">Whether italics formatting should be enabled or disabled for following text.</param>
public EmphasisItalicPayload(bool enabled)
{
this.IsEnabled = enabled;
}
/// <summary>
/// Initializes a new instance of the <see cref="EmphasisItalicPayload"/> class.
/// Creates an EmphasisItalicPayload.
/// </summary>
internal EmphasisItalicPayload()
{
}
/// <summary>
/// Initializes a new instance of the <see cref="EmphasisItalicPayload"/> class.
/// Creates an EmphasisItalicPayload.
/// </summary>
internal EmphasisItalicPayload()
{
}
/// <summary>
/// Gets a payload representing enabling italics on following text.
/// </summary>
public static EmphasisItalicPayload ItalicsOn => new(true);
/// <summary>
/// Gets a payload representing enabling italics on following text.
/// </summary>
public static EmphasisItalicPayload ItalicsOn => new(true);
/// <summary>
/// Gets a payload representing disabling italics on following text.
/// </summary>
public static EmphasisItalicPayload ItalicsOff => new(false);
/// <summary>
/// Gets a payload representing disabling italics on following text.
/// </summary>
public static EmphasisItalicPayload ItalicsOff => new(false);
/// <summary>
/// Gets a value indicating whether this payload enables italics formatting for following text.
/// </summary>
public bool IsEnabled { get; private set; }
/// <summary>
/// Gets a value indicating whether this payload enables italics formatting for following text.
/// </summary>
public bool IsEnabled { get; private set; }
/// <inheritdoc/>
public override PayloadType Type => PayloadType.EmphasisItalic;
/// <inheritdoc/>
public override PayloadType Type => PayloadType.EmphasisItalic;
/// <inheritdoc/>
public override string ToString()
{
return $"{this.Type} - Enabled: {this.IsEnabled}";
}
/// <inheritdoc/>
public override string ToString()
{
return $"{this.Type} - Enabled: {this.IsEnabled}";
}
/// <inheritdoc/>
protected override byte[] EncodeImpl()
{
// realistically this will always be a single byte of value 1 or 2
// but we'll treat it normally anyway
var enabledBytes = MakeInteger(this.IsEnabled ? 1u : 0);
/// <inheritdoc/>
protected override byte[] EncodeImpl()
{
// realistically this will always be a single byte of value 1 or 2
// but we'll treat it normally anyway
var enabledBytes = MakeInteger(this.IsEnabled ? 1u : 0);
var chunkLen = enabledBytes.Length + 1;
var bytes = new List<byte>()
var chunkLen = enabledBytes.Length + 1;
var bytes = new List<byte>()
{
START_BYTE, (byte)SeStringChunkType.EmphasisItalic, (byte)chunkLen,
};
bytes.AddRange(enabledBytes);
bytes.Add(END_BYTE);
bytes.AddRange(enabledBytes);
bytes.Add(END_BYTE);
return bytes.ToArray();
}
return bytes.ToArray();
}
/// <inheritdoc/>
protected override void DecodeImpl(BinaryReader reader, long endOfStream)
{
this.IsEnabled = GetInteger(reader) == 1;
/// <inheritdoc/>
protected override void DecodeImpl(BinaryReader reader, long endOfStream)
{
this.IsEnabled = GetInteger(reader) == 1;
}
}
}

View file

@ -1,62 +1,63 @@
using System.Collections.Generic;
using System.IO;
namespace Dalamud.Game.Text.SeStringHandling.Payloads;
/// <summary>
/// SeString payload representing a bitmap icon from fontIcon.
/// </summary>
public class IconPayload : Payload
namespace Dalamud.Game.Text.SeStringHandling.Payloads
{
/// <summary>
/// Initializes a new instance of the <see cref="IconPayload"/> class.
/// Create a Icon payload for the specified icon.
/// SeString payload representing a bitmap icon from fontIcon.
/// </summary>
/// <param name="icon">The Icon.</param>
public IconPayload(BitmapFontIcon icon)
public class IconPayload : Payload
{
this.Icon = icon;
}
/// <summary>
/// Initializes a new instance of the <see cref="IconPayload"/> class.
/// Create a Icon payload for the specified icon.
/// </summary>
internal IconPayload()
{
}
/// <inheritdoc/>
public override PayloadType Type => PayloadType.Icon;
/// <summary>
/// Gets or sets the icon the payload represents.
/// </summary>
public BitmapFontIcon Icon { get; set; } = BitmapFontIcon.None;
/// <inheritdoc />
public override string ToString()
{
return $"{this.Type} - {this.Icon}";
}
/// <inheritdoc />
protected override byte[] EncodeImpl()
{
var indexBytes = MakeInteger((uint)this.Icon);
var chunkLen = indexBytes.Length + 1;
var bytes = new List<byte>(new byte[]
/// <summary>
/// Initializes a new instance of the <see cref="IconPayload"/> class.
/// Create a Icon payload for the specified icon.
/// </summary>
/// <param name="icon">The Icon.</param>
public IconPayload(BitmapFontIcon icon)
{
START_BYTE, (byte)SeStringChunkType.Icon, (byte)chunkLen,
});
bytes.AddRange(indexBytes);
bytes.Add(END_BYTE);
return bytes.ToArray();
}
this.Icon = icon;
}
/// <inheritdoc />
protected override void DecodeImpl(BinaryReader reader, long endOfStream)
{
this.Icon = (BitmapFontIcon)GetInteger(reader);
/// <summary>
/// Initializes a new instance of the <see cref="IconPayload"/> class.
/// Create a Icon payload for the specified icon.
/// </summary>
internal IconPayload()
{
}
/// <inheritdoc/>
public override PayloadType Type => PayloadType.Icon;
/// <summary>
/// Gets or sets the icon the payload represents.
/// </summary>
public BitmapFontIcon Icon { get; set; } = BitmapFontIcon.None;
/// <inheritdoc />
public override string ToString()
{
return $"{this.Type} - {this.Icon}";
}
/// <inheritdoc />
protected override byte[] EncodeImpl()
{
var indexBytes = MakeInteger((uint)this.Icon);
var chunkLen = indexBytes.Length + 1;
var bytes = new List<byte>(new byte[]
{
START_BYTE, (byte)SeStringChunkType.Icon, (byte)chunkLen,
});
bytes.AddRange(indexBytes);
bytes.Add(END_BYTE);
return bytes.ToArray();
}
/// <inheritdoc />
protected override void DecodeImpl(BinaryReader reader, long endOfStream)
{
this.Icon = (BitmapFontIcon)GetInteger(reader);
}
}
}

View file

@ -6,183 +6,184 @@ using System.Text;
using Lumina.Excel.GeneratedSheets;
using Newtonsoft.Json;
namespace Dalamud.Game.Text.SeStringHandling.Payloads;
/// <summary>
/// An SeString Payload representing an interactable item link.
/// </summary>
public class ItemPayload : Payload
namespace Dalamud.Game.Text.SeStringHandling.Payloads
{
private Item item;
// 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 = null;
[JsonProperty]
private uint itemId;
/// <summary>
/// Initializes a new instance of the <see cref="ItemPayload"/> class.
/// Creates a payload representing an interactable item link for the specified item.
/// An SeString Payload representing an interactable item link.
/// </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)
public class ItemPayload : Payload
{
this.itemId = itemId;
this.IsHQ = isHQ;
this.displayName = displayNameOverride;
}
private Item item;
/// <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()
{
}
// 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 = null;
/// <inheritdoc/>
public override PayloadType Type => PayloadType.Item;
[JsonProperty]
private uint itemId;
/// <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
/// <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)
{
return this.displayName;
this.itemId = itemId;
this.IsHQ = isHQ;
this.displayName = displayNameOverride;
}
set
/// <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()
{
this.displayName = value;
this.Dirty = true;
}
}
/// <summary>
/// Gets the raw item ID of this payload.
/// </summary>
[JsonIgnore]
public uint ItemId => this.itemId;
/// <inheritdoc/>
public override PayloadType Type => PayloadType.Item;
/// <summary>
/// Gets the underlying Lumina Item represented by this payload.
/// </summary>
/// <remarks>
/// The value is evaluated lazily and cached.
/// </remarks>
[JsonIgnore]
public Item Item => this.item ??= this.DataResolver.GetExcelSheet<Item>().GetRow(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 { get; private set; } = false;
/// <inheritdoc/>
public override string ToString()
{
return $"{this.Type} - ItemId: {this.itemId}, IsHQ: {this.IsHQ}, Name: {this.displayName ?? this.Item.Name}";
}
/// <inheritdoc/>
protected override byte[] EncodeImpl()
{
var actualItemId = this.IsHQ ? this.itemId + 1000000 : this.itemId;
var idBytes = MakeInteger(actualItemId);
var hasName = !string.IsNullOrEmpty(this.displayName);
var chunkLen = idBytes.Length + 4;
if (hasName)
/// <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
{
// 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)
get
{
chunkLen += 4; // unicode representation of the HQ symbol is 3 bytes, preceded by a space
return this.displayName;
}
set
{
this.displayName = value;
this.Dirty = true;
}
}
var bytes = new List<byte>()
/// <summary>
/// Gets the raw item ID of this payload.
/// </summary>
[JsonIgnore]
public uint ItemId => this.itemId;
/// <summary>
/// Gets the underlying Lumina Item represented by this payload.
/// </summary>
/// <remarks>
/// The value is evaluated lazily and cached.
/// </remarks>
[JsonIgnore]
public Item Item => this.item ??= this.DataResolver.GetExcelSheet<Item>().GetRow(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 { get; private set; } = false;
/// <inheritdoc/>
public override string ToString()
{
return $"{this.Type} - ItemId: {this.itemId}, IsHQ: {this.IsHQ}, Name: {this.displayName ?? this.Item.Name}";
}
/// <inheritdoc/>
protected override byte[] EncodeImpl()
{
var actualItemId = this.IsHQ ? this.itemId + 1000000 : this.itemId;
var idBytes = MakeInteger(actualItemId);
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 });
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)
// Links don't have to include the name, but if they do, it requires additional work
if (hasName)
{
nameLen += 4; // space plus 3 bytes for HQ symbol
}
var nameLen = this.displayName.Length + 1;
if (this.IsHQ)
{
nameLen += 4; // space plus 3 bytes for HQ symbol
}
bytes.AddRange(new byte[]
{
bytes.AddRange(new byte[]
{
0xFF, // unk
(byte)nameLen,
});
bytes.AddRange(Encoding.UTF8.GetBytes(this.displayName));
});
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.itemId = GetInteger(reader);
if (this.itemId > 1000000)
{
this.itemId -= 1000000;
this.IsHQ = true;
}
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();
if (this.IsHQ)
{
// space and HQ symbol
bytes.AddRange(new byte[] { 0x20, 0xEE, 0x80, 0xBC });
}
}
this.displayName = Encoding.UTF8.GetString(itemNameBytes);
bytes.Add(END_BYTE);
return bytes.ToArray();
}
/// <inheritdoc/>
protected override void DecodeImpl(BinaryReader reader, long endOfStream)
{
this.itemId = GetInteger(reader);
if (this.itemId > 1000000)
{
this.itemId -= 1000000;
this.IsHQ = true;
}
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);
}
}
}
}

View file

@ -5,231 +5,232 @@ using System.IO;
using Lumina.Excel.GeneratedSheets;
using Newtonsoft.Json;
namespace Dalamud.Game.Text.SeStringHandling.Payloads;
/// <summary>
/// An SeString Payload representing an interactable map position link.
/// </summary>
public class MapLinkPayload : Payload
namespace Dalamud.Game.Text.SeStringHandling.Payloads
{
private Map map;
private TerritoryType territoryType;
private string placeNameRegion;
private string placeName;
[JsonProperty]
private uint territoryTypeId;
[JsonProperty]
private uint mapId;
/// <summary>
/// Initializes a new instance of the <see cref="MapLinkPayload"/> class.
/// Creates an interactable MapLinkPayload from a human-readable position.
/// An SeString Payload representing an interactable map position link.
/// </summary>
/// <param name="territoryTypeId">The id of the TerritoryType entry for this link.</param>
/// <param name="mapId">The id of the Map entry for this link.</param>
/// <param name="niceXCoord">The human-readable x-coordinate for this link.</param>
/// <param name="niceYCoord">The human-readable y-coordinate for this link.</param>
/// <param name="fudgeFactor">An optional offset to account for rounding and truncation errors; it is best to leave this untouched in most cases.</param>
public MapLinkPayload(uint territoryTypeId, uint mapId, float niceXCoord, float niceYCoord, float fudgeFactor = 0.05f)
public class MapLinkPayload : Payload
{
this.territoryTypeId = territoryTypeId;
this.mapId = mapId;
// this fudge is necessary basically to ensure we don't shift down a full tenth
// because essentially values are truncated instead of rounded, so 3.09999f will become
// 3.0f and not 3.1f
this.RawX = this.ConvertMapCoordinateToRawPosition(niceXCoord + fudgeFactor, this.Map.SizeFactor);
this.RawY = this.ConvertMapCoordinateToRawPosition(niceYCoord + fudgeFactor, this.Map.SizeFactor);
}
private Map map;
private TerritoryType territoryType;
private string placeNameRegion;
private string placeName;
/// <summary>
/// Initializes a new instance of the <see cref="MapLinkPayload"/> class.
/// Creates an interactable MapLinkPayload from a raw position.
/// </summary>
/// <param name="territoryTypeId">The id of the TerritoryType entry for this link.</param>
/// <param name="mapId">The id of the Map entry for this link.</param>
/// <param name="rawX">The internal raw x-coordinate for this link.</param>
/// <param name="rawY">The internal raw y-coordinate for this link.</param>
public MapLinkPayload(uint territoryTypeId, uint mapId, int rawX, int rawY)
{
this.territoryTypeId = territoryTypeId;
this.mapId = mapId;
this.RawX = rawX;
this.RawY = rawY;
}
[JsonProperty]
private uint territoryTypeId;
/// <summary>
/// Initializes a new instance of the <see cref="MapLinkPayload"/> class.
/// Creates an interactable MapLinkPayload from a human-readable position.
/// </summary>
internal MapLinkPayload()
{
}
[JsonProperty]
private uint mapId;
/// <inheritdoc/>
public override PayloadType Type => PayloadType.MapLink;
/// <summary>
/// Gets the Map specified for this map link.
/// </summary>
/// <remarks>
/// The value is evaluated lazily and cached.
/// </remarks>
[JsonIgnore]
public Map Map => this.map ??= this.DataResolver.GetExcelSheet<Map>().GetRow(this.mapId);
/// <summary>
/// Gets the TerritoryType specified for this map link.
/// </summary>
/// <remarks>
/// The value is evaluated lazily and cached.
/// </remarks>
[JsonIgnore]
public TerritoryType TerritoryType => this.territoryType ??= this.DataResolver.GetExcelSheet<TerritoryType>().GetRow(this.territoryTypeId);
/// <summary>
/// Gets the internal x-coordinate for this map position.
/// </summary>
public int RawX { get; private set; }
/// <summary>
/// Gets the internal y-coordinate for this map position.
/// </summary>
public int RawY { get; private set; }
// these could be cached, but this isn't really too egregious
/// <summary>
/// Gets the readable x-coordinate position for this map link. This value is approximate and unrounded.
/// </summary>
public float XCoord => this.ConvertRawPositionToMapCoordinate(this.RawX, this.Map.SizeFactor);
/// <summary>
/// Gets the readable y-coordinate position for this map link. This value is approximate and unrounded.
/// </summary>
[JsonIgnore]
public float YCoord => this.ConvertRawPositionToMapCoordinate(this.RawY, this.Map.SizeFactor);
// there is no Z; it's purely in the text payload where applicable
/// <summary>
/// Gets the printable map coordinates for this link. This value tries to match the in-game printable text as closely
/// as possible but is an approximation and may be slightly off for some positions.
/// </summary>
[JsonIgnore]
public string CoordinateString
{
get
/// <summary>
/// Initializes a new instance of the <see cref="MapLinkPayload"/> class.
/// Creates an interactable MapLinkPayload from a human-readable position.
/// </summary>
/// <param name="territoryTypeId">The id of the TerritoryType entry for this link.</param>
/// <param name="mapId">The id of the Map entry for this link.</param>
/// <param name="niceXCoord">The human-readable x-coordinate for this link.</param>
/// <param name="niceYCoord">The human-readable y-coordinate for this link.</param>
/// <param name="fudgeFactor">An optional offset to account for rounding and truncation errors; it is best to leave this untouched in most cases.</param>
public MapLinkPayload(uint territoryTypeId, uint mapId, float niceXCoord, float niceYCoord, float fudgeFactor = 0.05f)
{
// this truncates the values to one decimal without rounding, which is what the game does
// the fudge also just attempts to correct the truncated/displayed value for rounding/fp issues
// TODO: should this fudge factor be the same as in the ctor? currently not since that is customizable
const float fudge = 0.02f;
var x = Math.Truncate((this.XCoord + fudge) * 10.0f) / 10.0f;
var y = Math.Truncate((this.YCoord + fudge) * 10.0f) / 10.0f;
// the formatting and spacing the game uses
return $"( {x:0.0} , {y:0.0} )";
this.territoryTypeId = territoryTypeId;
this.mapId = mapId;
// this fudge is necessary basically to ensure we don't shift down a full tenth
// because essentially values are truncated instead of rounded, so 3.09999f will become
// 3.0f and not 3.1f
this.RawX = this.ConvertMapCoordinateToRawPosition(niceXCoord + fudgeFactor, this.Map.SizeFactor);
this.RawY = this.ConvertMapCoordinateToRawPosition(niceYCoord + fudgeFactor, this.Map.SizeFactor);
}
}
/// <summary>
/// Gets the region name for this map link. This corresponds to the upper zone name found in the actual in-game map UI. eg, "La Noscea".
/// </summary>
[JsonIgnore]
public string PlaceNameRegion => this.placeNameRegion ??= this.TerritoryType.PlaceNameRegion.Value?.Name;
/// <summary>
/// Initializes a new instance of the <see cref="MapLinkPayload"/> class.
/// Creates an interactable MapLinkPayload from a raw position.
/// </summary>
/// <param name="territoryTypeId">The id of the TerritoryType entry for this link.</param>
/// <param name="mapId">The id of the Map entry for this link.</param>
/// <param name="rawX">The internal raw x-coordinate for this link.</param>
/// <param name="rawY">The internal raw y-coordinate for this link.</param>
public MapLinkPayload(uint territoryTypeId, uint mapId, int rawX, int rawY)
{
this.territoryTypeId = territoryTypeId;
this.mapId = mapId;
this.RawX = rawX;
this.RawY = rawY;
}
/// <summary>
/// Gets the place name for this map link. This corresponds to the lower zone name found in the actual in-game map UI. eg, "Limsa Lominsa Upper Decks".
/// </summary>
[JsonIgnore]
public string PlaceName => this.placeName ??= this.TerritoryType.PlaceName.Value?.Name;
/// <summary>
/// Initializes a new instance of the <see cref="MapLinkPayload"/> class.
/// Creates an interactable MapLinkPayload from a human-readable position.
/// </summary>
internal MapLinkPayload()
{
}
/// <summary>
/// Gets the data string for this map link, for use by internal game functions that take a string variant and not a binary payload.
/// </summary>
public string DataString => $"m:{this.TerritoryType.RowId},{this.Map.RowId},{this.RawX},{this.RawY}";
/// <inheritdoc/>
public override PayloadType Type => PayloadType.MapLink;
/// <inheritdoc/>
public override string ToString()
{
return $"{this.Type} - TerritoryTypeId: {this.territoryTypeId}, MapId: {this.mapId}, RawX: {this.RawX}, RawY: {this.RawY}, display: {this.PlaceName} {this.CoordinateString}";
}
/// <summary>
/// Gets the Map specified for this map link.
/// </summary>
/// <remarks>
/// The value is evaluated lazily and cached.
/// </remarks>
[JsonIgnore]
public Map Map => this.map ??= this.DataResolver.GetExcelSheet<Map>().GetRow(this.mapId);
/// <inheritdoc/>
protected override byte[] EncodeImpl()
{
var packedTerritoryAndMapBytes = MakePackedInteger(this.territoryTypeId, this.mapId);
var xBytes = MakeInteger(unchecked((uint)this.RawX));
var yBytes = MakeInteger(unchecked((uint)this.RawY));
/// <summary>
/// Gets the TerritoryType specified for this map link.
/// </summary>
/// <remarks>
/// The value is evaluated lazily and cached.
/// </remarks>
[JsonIgnore]
public TerritoryType TerritoryType => this.territoryType ??= this.DataResolver.GetExcelSheet<TerritoryType>().GetRow(this.territoryTypeId);
var chunkLen = 4 + packedTerritoryAndMapBytes.Length + xBytes.Length + yBytes.Length;
/// <summary>
/// Gets the internal x-coordinate for this map position.
/// </summary>
public int RawX { get; private set; }
var bytes = new List<byte>()
/// <summary>
/// Gets the internal y-coordinate for this map position.
/// </summary>
public int RawY { get; private set; }
// these could be cached, but this isn't really too egregious
/// <summary>
/// Gets the readable x-coordinate position for this map link. This value is approximate and unrounded.
/// </summary>
public float XCoord => this.ConvertRawPositionToMapCoordinate(this.RawX, this.Map.SizeFactor);
/// <summary>
/// Gets the readable y-coordinate position for this map link. This value is approximate and unrounded.
/// </summary>
[JsonIgnore]
public float YCoord => this.ConvertRawPositionToMapCoordinate(this.RawY, this.Map.SizeFactor);
// there is no Z; it's purely in the text payload where applicable
/// <summary>
/// Gets the printable map coordinates for this link. This value tries to match the in-game printable text as closely
/// as possible but is an approximation and may be slightly off for some positions.
/// </summary>
[JsonIgnore]
public string CoordinateString
{
get
{
// this truncates the values to one decimal without rounding, which is what the game does
// the fudge also just attempts to correct the truncated/displayed value for rounding/fp issues
// TODO: should this fudge factor be the same as in the ctor? currently not since that is customizable
const float fudge = 0.02f;
var x = Math.Truncate((this.XCoord + fudge) * 10.0f) / 10.0f;
var y = Math.Truncate((this.YCoord + fudge) * 10.0f) / 10.0f;
// the formatting and spacing the game uses
return $"( {x:0.0} , {y:0.0} )";
}
}
/// <summary>
/// Gets the region name for this map link. This corresponds to the upper zone name found in the actual in-game map UI. eg, "La Noscea".
/// </summary>
[JsonIgnore]
public string PlaceNameRegion => this.placeNameRegion ??= this.TerritoryType.PlaceNameRegion.Value?.Name;
/// <summary>
/// Gets the place name for this map link. This corresponds to the lower zone name found in the actual in-game map UI. eg, "Limsa Lominsa Upper Decks".
/// </summary>
[JsonIgnore]
public string PlaceName => this.placeName ??= this.TerritoryType.PlaceName.Value?.Name;
/// <summary>
/// Gets the data string for this map link, for use by internal game functions that take a string variant and not a binary payload.
/// </summary>
public string DataString => $"m:{this.TerritoryType.RowId},{this.Map.RowId},{this.RawX},{this.RawY}";
/// <inheritdoc/>
public override string ToString()
{
return $"{this.Type} - TerritoryTypeId: {this.territoryTypeId}, MapId: {this.mapId}, RawX: {this.RawX}, RawY: {this.RawY}, display: {this.PlaceName} {this.CoordinateString}";
}
/// <inheritdoc/>
protected override byte[] EncodeImpl()
{
var packedTerritoryAndMapBytes = MakePackedInteger(this.territoryTypeId, this.mapId);
var xBytes = MakeInteger(unchecked((uint)this.RawX));
var yBytes = MakeInteger(unchecked((uint)this.RawY));
var chunkLen = 4 + packedTerritoryAndMapBytes.Length + xBytes.Length + yBytes.Length;
var bytes = new List<byte>()
{
START_BYTE,
(byte)SeStringChunkType.Interactable, (byte)chunkLen, (byte)EmbeddedInfoType.MapPositionLink,
};
bytes.AddRange(packedTerritoryAndMapBytes);
bytes.AddRange(xBytes);
bytes.AddRange(yBytes);
bytes.AddRange(packedTerritoryAndMapBytes);
bytes.AddRange(xBytes);
bytes.AddRange(yBytes);
// unk
bytes.AddRange(new byte[] { 0xFF, 0x01, END_BYTE });
// unk
bytes.AddRange(new byte[] { 0xFF, 0x01, END_BYTE });
return bytes.ToArray();
}
/// <inheritdoc/>
protected override void DecodeImpl(BinaryReader reader, long endOfStream)
{
// for debugging for now
var oldPos = reader.BaseStream.Position;
var bytes = reader.ReadBytes((int)(endOfStream - reader.BaseStream.Position));
reader.BaseStream.Position = oldPos;
try
{
(this.territoryTypeId, this.mapId) = GetPackedIntegers(reader);
this.RawX = unchecked((int)GetInteger(reader));
this.RawY = unchecked((int)GetInteger(reader));
// the Z coordinate is never in this chunk, just the text (if applicable)
// seems to always be FF 01
reader.ReadBytes(2);
return bytes.ToArray();
}
catch (NotSupportedException)
/// <inheritdoc/>
protected override void DecodeImpl(BinaryReader reader, long endOfStream)
{
Serilog.Log.Information($"Unsupported map bytes {BitConverter.ToString(bytes).Replace("-", " ")}");
// we still want to break here for now, or we'd just throw again later
throw;
// for debugging for now
var oldPos = reader.BaseStream.Position;
var bytes = reader.ReadBytes((int)(endOfStream - reader.BaseStream.Position));
reader.BaseStream.Position = oldPos;
try
{
(this.territoryTypeId, this.mapId) = GetPackedIntegers(reader);
this.RawX = unchecked((int)GetInteger(reader));
this.RawY = unchecked((int)GetInteger(reader));
// the Z coordinate is never in this chunk, just the text (if applicable)
// seems to always be FF 01
reader.ReadBytes(2);
}
catch (NotSupportedException)
{
Serilog.Log.Information($"Unsupported map bytes {BitConverter.ToString(bytes).Replace("-", " ")}");
// we still want to break here for now, or we'd just throw again later
throw;
}
}
#region ugliness
// from https://github.com/xivapi/ffxiv-datamining/blob/master/docs/MapCoordinates.md
// extra 1/1000 because that is how the network ints are done
private float ConvertRawPositionToMapCoordinate(int pos, float scale)
{
var c = scale / 100.0f;
var scaledPos = pos * c / 1000.0f;
return (41.0f / c * ((scaledPos + 1024.0f) / 2048.0f)) + 1.0f;
}
// Created as the inverse of ConvertRawPositionToMapCoordinate(), since no one seemed to have a version of that
private int ConvertMapCoordinateToRawPosition(float pos, float scale)
{
var c = scale / 100.0f;
var scaledPos = (((pos - 1.0f) * c / 41.0f * 2048.0f) - 1024.0f) / c;
scaledPos *= 1000.0f;
return (int)scaledPos;
}
#endregion
}
#region ugliness
// from https://github.com/xivapi/ffxiv-datamining/blob/master/docs/MapCoordinates.md
// extra 1/1000 because that is how the network ints are done
private float ConvertRawPositionToMapCoordinate(int pos, float scale)
{
var c = scale / 100.0f;
var scaledPos = pos * c / 1000.0f;
return (41.0f / c * ((scaledPos + 1024.0f) / 2048.0f)) + 1.0f;
}
// Created as the inverse of ConvertRawPositionToMapCoordinate(), since no one seemed to have a version of that
private int ConvertMapCoordinateToRawPosition(float pos, float scale)
{
var c = scale / 100.0f;
var scaledPos = (((pos - 1.0f) * c / 41.0f * 2048.0f) - 1024.0f) / c;
scaledPos *= 1000.0f;
return (int)scaledPos;
}
#endregion
}

View file

@ -1,33 +1,34 @@
using System;
using System.IO;
namespace Dalamud.Game.Text.SeStringHandling.Payloads;
/// <summary>
/// A wrapped newline character.
/// </summary>
public class NewLinePayload : Payload, ITextProvider
namespace Dalamud.Game.Text.SeStringHandling.Payloads
{
private readonly byte[] bytes = { START_BYTE, (byte)SeStringChunkType.NewLine, 0x01, END_BYTE };
/// <summary>
/// Gets an instance of NewLinePayload.
/// A wrapped newline character.
/// </summary>
public static NewLinePayload Payload => new();
/// <summary>
/// Gets the text of this payload, evaluates to <c>Environment.NewLine</c>.
/// </summary>
public string Text => Environment.NewLine;
/// <inheritdoc/>
public override PayloadType Type => PayloadType.NewLine;
/// <inheritdoc/>
protected override byte[] EncodeImpl() => this.bytes;
/// <inheritdoc/>
protected override void DecodeImpl(BinaryReader reader, long endOfStream)
public class NewLinePayload : Payload, ITextProvider
{
private readonly byte[] bytes = { START_BYTE, (byte)SeStringChunkType.NewLine, 0x01, END_BYTE };
/// <summary>
/// Gets an instance of NewLinePayload.
/// </summary>
public static NewLinePayload Payload => new();
/// <summary>
/// Gets the text of this payload, evaluates to <c>Environment.NewLine</c>.
/// </summary>
public string Text => Environment.NewLine;
/// <inheritdoc/>
public override PayloadType Type => PayloadType.NewLine;
/// <inheritdoc/>
protected override byte[] EncodeImpl() => this.bytes;
/// <inheritdoc/>
protected override void DecodeImpl(BinaryReader reader, long endOfStream)
{
}
}
}

View file

@ -5,89 +5,89 @@ using System.Text;
using Lumina.Excel.GeneratedSheets;
using Newtonsoft.Json;
namespace Dalamud.Game.Text.SeStringHandling.Payloads;
/// <summary>
/// An SeString Payload representing a player link.
/// </summary>
public class PlayerPayload : Payload
namespace Dalamud.Game.Text.SeStringHandling.Payloads
{
private World world;
[JsonProperty]
private uint serverId;
[JsonProperty]
private string playerName;
/// <summary>
/// Initializes a new instance of the <see cref="PlayerPayload"/> class.
/// Create a PlayerPayload link for the specified player.
/// An SeString Payload representing a player link.
/// </summary>
/// <param name="playerName">The player's displayed name.</param>
/// <param name="serverId">The player's home server id.</param>
public PlayerPayload(string playerName, uint serverId)
public class PlayerPayload : Payload
{
this.playerName = playerName;
this.serverId = serverId;
}
private World world;
/// <summary>
/// Initializes a new instance of the <see cref="PlayerPayload"/> class.
/// Create a PlayerPayload link for the specified player.
/// </summary>
internal PlayerPayload()
{
}
[JsonProperty]
private uint serverId;
/// <summary>
/// Gets the Lumina object representing the player's home server.
/// </summary>
/// <remarks>
/// Value is evaluated lazily and cached.
/// </remarks>
[JsonIgnore]
public World World => this.world ??= this.DataResolver.GetExcelSheet<World>().GetRow(this.serverId);
[JsonProperty]
private string playerName;
/// <summary>
/// Gets or sets the player's displayed name. This does not contain the server name.
/// </summary>
[JsonIgnore]
public string PlayerName
{
get
/// <summary>
/// Initializes a new instance of the <see cref="PlayerPayload"/> class.
/// Create a PlayerPayload link for the specified player.
/// </summary>
/// <param name="playerName">The player's displayed name.</param>
/// <param name="serverId">The player's home server id.</param>
public PlayerPayload(string playerName, uint serverId)
{
return this.playerName;
this.playerName = playerName;
this.serverId = serverId;
}
set
/// <summary>
/// Initializes a new instance of the <see cref="PlayerPayload"/> class.
/// Create a PlayerPayload link for the specified player.
/// </summary>
internal PlayerPayload()
{
this.playerName = value;
this.Dirty = true;
}
}
/// <summary>
/// Gets the text representation of this player link matching how it might appear in-game.
/// The world name will always be present.
/// </summary>
[JsonIgnore]
public string DisplayedName => $"{this.PlayerName}{(char)SeIconChar.CrossWorld}{this.World.Name}";
/// <summary>
/// Gets the Lumina object representing the player's home server.
/// </summary>
/// <remarks>
/// Value is evaluated lazily and cached.
/// </remarks>
[JsonIgnore]
public World World => this.world ??= this.DataResolver.GetExcelSheet<World>().GetRow(this.serverId);
/// <inheritdoc/>
public override PayloadType Type => PayloadType.Player;
/// <summary>
/// Gets or sets the player's displayed name. This does not contain the server name.
/// </summary>
[JsonIgnore]
public string PlayerName
{
get
{
return this.playerName;
}
/// <inheritdoc/>
public override string ToString()
{
return $"{this.Type} - PlayerName: {this.PlayerName}, ServerId: {this.serverId}, ServerName: {this.World.Name}";
}
set
{
this.playerName = value;
this.Dirty = true;
}
}
/// <inheritdoc/>
protected override byte[] EncodeImpl()
{
var chunkLen = this.playerName.Length + 7;
var bytes = new List<byte>()
/// <summary>
/// Gets the text representation of this player link matching how it might appear in-game.
/// The world name will always be present.
/// </summary>
[JsonIgnore]
public string DisplayedName => $"{this.PlayerName}{(char)SeIconChar.CrossWorld}{this.World.Name}";
/// <inheritdoc/>
public override PayloadType Type => PayloadType.Player;
/// <inheritdoc/>
public override string ToString()
{
return $"{this.Type} - PlayerName: {this.PlayerName}, ServerId: {this.serverId}, ServerName: {this.World.Name}";
}
/// <inheritdoc/>
protected override byte[] EncodeImpl()
{
var chunkLen = this.playerName.Length + 7;
var bytes = new List<byte>()
{
START_BYTE,
(byte)SeStringChunkType.Interactable, (byte)chunkLen, (byte)EmbeddedInfoType.PlayerName,
@ -98,38 +98,39 @@ public class PlayerPayload : Payload
(byte)(this.playerName.Length + 1),
};
bytes.AddRange(Encoding.UTF8.GetBytes(this.playerName));
bytes.Add(END_BYTE);
bytes.AddRange(Encoding.UTF8.GetBytes(this.playerName));
bytes.Add(END_BYTE);
// TODO: should these really be here? additional payloads should come in separately already...
// TODO: should these really be here? additional payloads should come in separately already...
// encoded names are followed by the name in plain text again
// use the payload parsing for consistency, as this is technically a new chunk
bytes.AddRange(new TextPayload(this.playerName).Encode());
// encoded names are followed by the name in plain text again
// use the payload parsing for consistency, as this is technically a new chunk
bytes.AddRange(new TextPayload(this.playerName).Encode());
// unsure about this entire packet, but it seems to always follow a name
bytes.AddRange(new byte[]
{
// unsure about this entire packet, but it seems to always follow a name
bytes.AddRange(new byte[]
{
START_BYTE, (byte)SeStringChunkType.Interactable, 0x07, (byte)EmbeddedInfoType.LinkTerminator,
0x01, 0x01, 0x01, 0xFF, 0x01,
END_BYTE,
});
});
return bytes.ToArray();
}
return bytes.ToArray();
}
/// <inheritdoc/>
protected override void DecodeImpl(BinaryReader reader, long endOfStream)
{
// unk
reader.ReadByte();
/// <inheritdoc/>
protected override void DecodeImpl(BinaryReader reader, long endOfStream)
{
// unk
reader.ReadByte();
this.serverId = GetInteger(reader);
this.serverId = GetInteger(reader);
// unk
reader.ReadBytes(2);
// unk
reader.ReadBytes(2);
var nameLen = (int)GetInteger(reader);
this.playerName = Encoding.UTF8.GetString(reader.ReadBytes(nameLen));
var nameLen = (int)GetInteger(reader);
this.playerName = Encoding.UTF8.GetString(reader.ReadBytes(nameLen));
}
}
}

View file

@ -4,74 +4,75 @@ using System.IO;
using Lumina.Excel.GeneratedSheets;
using Newtonsoft.Json;
namespace Dalamud.Game.Text.SeStringHandling.Payloads;
/// <summary>
/// An SeString Payload representing an interactable quest link.
/// </summary>
public class QuestPayload : Payload
namespace Dalamud.Game.Text.SeStringHandling.Payloads
{
private Quest quest;
[JsonProperty]
private uint questId;
/// <summary>
/// Initializes a new instance of the <see cref="QuestPayload"/> class.
/// Creates a payload representing an interactable quest link for the specified quest.
/// An SeString Payload representing an interactable quest link.
/// </summary>
/// <param name="questId">The id of the quest.</param>
public QuestPayload(uint questId)
public class QuestPayload : Payload
{
this.questId = questId;
}
private Quest quest;
/// <summary>
/// Initializes a new instance of the <see cref="QuestPayload"/> class.
/// Creates a payload representing an interactable quest link for the specified quest.
/// </summary>
internal QuestPayload()
{
}
[JsonProperty]
private uint questId;
/// <inheritdoc/>
public override PayloadType Type => PayloadType.Quest;
/// <summary>
/// Initializes a new instance of the <see cref="QuestPayload"/> class.
/// Creates a payload representing an interactable quest link for the specified quest.
/// </summary>
/// <param name="questId">The id of the quest.</param>
public QuestPayload(uint questId)
{
this.questId = questId;
}
/// <summary>
/// Gets the underlying Lumina Quest represented by this payload.
/// </summary>
/// <remarks>
/// The value is evaluated lazily and cached.
/// </remarks>
[JsonIgnore]
public Quest Quest => this.quest ??= this.DataResolver.GetExcelSheet<Quest>().GetRow(this.questId);
/// <summary>
/// Initializes a new instance of the <see cref="QuestPayload"/> class.
/// Creates a payload representing an interactable quest link for the specified quest.
/// </summary>
internal QuestPayload()
{
}
/// <inheritdoc />
public override string ToString()
{
return $"{this.Type} - QuestId: {this.questId}, Name: {this.Quest?.Name ?? "QUEST NOT FOUND"}";
}
/// <inheritdoc/>
public override PayloadType Type => PayloadType.Quest;
/// <inheritdoc/>
protected override byte[] EncodeImpl()
{
var idBytes = MakeInteger((ushort)this.questId);
var chunkLen = idBytes.Length + 4;
/// <summary>
/// Gets the underlying Lumina Quest represented by this payload.
/// </summary>
/// <remarks>
/// The value is evaluated lazily and cached.
/// </remarks>
[JsonIgnore]
public Quest Quest => this.quest ??= this.DataResolver.GetExcelSheet<Quest>().GetRow(this.questId);
var bytes = new List<byte>()
/// <inheritdoc />
public override string ToString()
{
return $"{this.Type} - QuestId: {this.questId}, Name: {this.Quest?.Name ?? "QUEST NOT FOUND"}";
}
/// <inheritdoc/>
protected override byte[] EncodeImpl()
{
var idBytes = MakeInteger((ushort)this.questId);
var chunkLen = idBytes.Length + 4;
var bytes = new List<byte>()
{
START_BYTE, (byte)SeStringChunkType.Interactable, (byte)chunkLen, (byte)EmbeddedInfoType.QuestLink,
};
bytes.AddRange(idBytes);
bytes.AddRange(new byte[] { 0x01, 0x01, END_BYTE });
return bytes.ToArray();
}
bytes.AddRange(idBytes);
bytes.AddRange(new byte[] { 0x01, 0x01, END_BYTE });
return bytes.ToArray();
}
/// <inheritdoc/>
protected override void DecodeImpl(BinaryReader reader, long endOfStream)
{
// Game uses int16, Luimina uses int32
this.questId = GetInteger(reader) + 65536;
/// <inheritdoc/>
protected override void DecodeImpl(BinaryReader reader, long endOfStream)
{
// Game uses int16, Luimina uses int32
this.questId = GetInteger(reader) + 65536;
}
}
}

View file

@ -5,115 +5,116 @@ using System.Linq;
using Newtonsoft.Json;
namespace Dalamud.Game.Text.SeStringHandling.Payloads;
/// <summary>
/// An SeString Payload representing unhandled raw payload data.
/// Mainly useful for constructing unhandled hardcoded payloads, or forwarding any unknown
/// payloads without modification.
/// </summary>
public class RawPayload : Payload
namespace Dalamud.Game.Text.SeStringHandling.Payloads
{
[JsonProperty]
private byte chunkType;
[JsonProperty]
private byte[] data;
/// <summary>
/// Initializes a new instance of the <see cref="RawPayload"/> class.
/// An SeString Payload representing unhandled raw payload data.
/// Mainly useful for constructing unhandled hardcoded payloads, or forwarding any unknown
/// payloads without modification.
/// </summary>
/// <param name="data">The payload data.</param>
public RawPayload(byte[] data)
public class RawPayload : Payload
{
// this payload is 'special' in that we require the entire chunk to be passed in
// and not just the data after the header
// This sets data to hold the chunk data fter the header, excluding the END_BYTE
this.chunkType = data[1];
this.data = data.Skip(3).Take(data.Length - 4).ToArray();
}
[JsonProperty]
private byte chunkType;
/// <summary>
/// Initializes a new instance of the <see cref="RawPayload"/> class.
/// </summary>
/// <param name="chunkType">The chunk type.</param>
[JsonConstructor]
internal RawPayload(byte chunkType)
{
this.chunkType = chunkType;
}
[JsonProperty]
private byte[] data;
/// <summary>
/// Gets a fixed Payload representing a common link-termination sequence, found in many payload chains.
/// </summary>
public static RawPayload LinkTerminator => new(new byte[] { 0x02, 0x27, 0x07, 0xCF, 0x01, 0x01, 0x01, 0xFF, 0x01, 0x03 });
/// <inheritdoc/>
public override PayloadType Type => PayloadType.Unknown;
/// <summary>
/// Gets the entire payload byte sequence for this payload.
/// The returned data is a clone and modifications will not be persisted.
/// </summary>
[JsonIgnore]
public byte[] Data
{
// this is a bit different from the underlying data
// We need to store just the chunk data for decode to behave nicely, but when reading data out
// it makes more sense to get the entire payload
get
/// <summary>
/// Initializes a new instance of the <see cref="RawPayload"/> class.
/// </summary>
/// <param name="data">The payload data.</param>
public RawPayload(byte[] data)
{
// for now don't allow modifying the contents
// because we don't really have a way to track Dirty
return (byte[])this.Encode().Clone();
}
}
/// <inheritdoc/>
public override bool Equals(object obj)
{
if (obj is RawPayload rp)
{
if (rp.Data.Length != this.Data.Length) return false;
return !this.Data.Where((t, i) => rp.Data[i] != t).Any();
// this payload is 'special' in that we require the entire chunk to be passed in
// and not just the data after the header
// This sets data to hold the chunk data fter the header, excluding the END_BYTE
this.chunkType = data[1];
this.data = data.Skip(3).Take(data.Length - 4).ToArray();
}
return false;
}
/// <summary>
/// Initializes a new instance of the <see cref="RawPayload"/> class.
/// </summary>
/// <param name="chunkType">The chunk type.</param>
[JsonConstructor]
internal RawPayload(byte chunkType)
{
this.chunkType = chunkType;
}
/// <inheritdoc/>
public override int GetHashCode()
{
return HashCode.Combine(this.Type, this.chunkType, this.data);
}
/// <summary>
/// Gets a fixed Payload representing a common link-termination sequence, found in many payload chains.
/// </summary>
public static RawPayload LinkTerminator => new(new byte[] { 0x02, 0x27, 0x07, 0xCF, 0x01, 0x01, 0x01, 0xFF, 0x01, 0x03 });
/// <inheritdoc/>
public override string ToString()
{
return $"{this.Type} - Data: {BitConverter.ToString(this.Data).Replace("-", " ")}";
}
/// <inheritdoc/>
public override PayloadType Type => PayloadType.Unknown;
/// <inheritdoc/>
protected override byte[] EncodeImpl()
{
var chunkLen = this.data.Length + 1;
/// <summary>
/// Gets the entire payload byte sequence for this payload.
/// The returned data is a clone and modifications will not be persisted.
/// </summary>
[JsonIgnore]
public byte[] Data
{
// this is a bit different from the underlying data
// We need to store just the chunk data for decode to behave nicely, but when reading data out
// it makes more sense to get the entire payload
get
{
// for now don't allow modifying the contents
// because we don't really have a way to track Dirty
return (byte[])this.Encode().Clone();
}
}
var bytes = new List<byte>()
/// <inheritdoc/>
public override bool Equals(object obj)
{
if (obj is RawPayload rp)
{
if (rp.Data.Length != this.Data.Length) return false;
return !this.Data.Where((t, i) => rp.Data[i] != t).Any();
}
return false;
}
/// <inheritdoc/>
public override int GetHashCode()
{
return HashCode.Combine(this.Type, this.chunkType, this.data);
}
/// <inheritdoc/>
public override string ToString()
{
return $"{this.Type} - Data: {BitConverter.ToString(this.Data).Replace("-", " ")}";
}
/// <inheritdoc/>
protected override byte[] EncodeImpl()
{
var chunkLen = this.data.Length + 1;
var bytes = new List<byte>()
{
START_BYTE,
this.chunkType,
(byte)chunkLen,
};
bytes.AddRange(this.data);
bytes.AddRange(this.data);
bytes.Add(END_BYTE);
bytes.Add(END_BYTE);
return bytes.ToArray();
}
return bytes.ToArray();
}
/// <inheritdoc/>
protected override void DecodeImpl(BinaryReader reader, long endOfStream)
{
this.data = reader.ReadBytes((int)(endOfStream - reader.BaseStream.Position + 1));
/// <inheritdoc/>
protected override void DecodeImpl(BinaryReader reader, long endOfStream)
{
this.data = reader.ReadBytes((int)(endOfStream - reader.BaseStream.Position + 1));
}
}
}

View file

@ -1,32 +1,33 @@
using System.IO;
namespace Dalamud.Game.Text.SeStringHandling.Payloads;
/// <summary>
/// A wrapped ''.
/// </summary>
public class SeHyphenPayload : Payload, ITextProvider
namespace Dalamud.Game.Text.SeStringHandling.Payloads
{
private readonly byte[] bytes = { START_BYTE, (byte)SeStringChunkType.SeHyphen, 0x01, END_BYTE };
/// <summary>
/// Gets an instance of SeHyphenPayload.
/// A wrapped ''.
/// </summary>
public static SeHyphenPayload Payload => new();
/// <summary>
/// Gets the text, just a ''.
/// </summary>
public string Text => "";
/// <inheritdoc/>
public override PayloadType Type => PayloadType.SeHyphen;
/// <inheritdoc />
protected override byte[] EncodeImpl() => this.bytes;
/// <inheritdoc />
protected override void DecodeImpl(BinaryReader reader, long endOfStream)
public class SeHyphenPayload : Payload, ITextProvider
{
private readonly byte[] bytes = { START_BYTE, (byte)SeStringChunkType.SeHyphen, 0x01, END_BYTE };
/// <summary>
/// Gets an instance of SeHyphenPayload.
/// </summary>
public static SeHyphenPayload Payload => new();
/// <summary>
/// Gets the text, just a ''.
/// </summary>
public string Text => "";
/// <inheritdoc/>
public override PayloadType Type => PayloadType.SeHyphen;
/// <inheritdoc />
protected override byte[] EncodeImpl() => this.bytes;
/// <inheritdoc />
protected override void DecodeImpl(BinaryReader reader, long endOfStream)
{
}
}
}

View file

@ -4,75 +4,76 @@ using System.IO;
using Lumina.Excel.GeneratedSheets;
using Newtonsoft.Json;
namespace Dalamud.Game.Text.SeStringHandling.Payloads;
/// <summary>
/// An SeString Payload representing an interactable status link.
/// </summary>
public class StatusPayload : Payload
namespace Dalamud.Game.Text.SeStringHandling.Payloads
{
private Status status;
[JsonProperty]
private uint statusId;
/// <summary>
/// Initializes a new instance of the <see cref="StatusPayload"/> class.
/// Creates a new StatusPayload for the given status id.
/// An SeString Payload representing an interactable status link.
/// </summary>
/// <param name="statusId">The id of the Status for this link.</param>
public StatusPayload(uint statusId)
public class StatusPayload : Payload
{
this.statusId = statusId;
}
private Status status;
/// <summary>
/// Initializes a new instance of the <see cref="StatusPayload"/> class.
/// Creates a new StatusPayload for the given status id.
/// </summary>
internal StatusPayload()
{
}
[JsonProperty]
private uint statusId;
/// <inheritdoc/>
public override PayloadType Type => PayloadType.Status;
/// <summary>
/// Initializes a new instance of the <see cref="StatusPayload"/> class.
/// Creates a new StatusPayload for the given status id.
/// </summary>
/// <param name="statusId">The id of the Status for this link.</param>
public StatusPayload(uint statusId)
{
this.statusId = statusId;
}
/// <summary>
/// Gets the Lumina Status object represented by this payload.
/// </summary>
/// <remarks>
/// The value is evaluated lazily and cached.
/// </remarks>
[JsonIgnore]
public Status Status => this.status ??= this.DataResolver.GetExcelSheet<Status>().GetRow(this.statusId);
/// <summary>
/// Initializes a new instance of the <see cref="StatusPayload"/> class.
/// Creates a new StatusPayload for the given status id.
/// </summary>
internal StatusPayload()
{
}
/// <inheritdoc/>
public override string ToString()
{
return $"{this.Type} - StatusId: {this.statusId}, Name: {this.Status.Name}";
}
/// <inheritdoc/>
public override PayloadType Type => PayloadType.Status;
/// <inheritdoc/>
protected override byte[] EncodeImpl()
{
var idBytes = MakeInteger(this.statusId);
/// <summary>
/// Gets the Lumina Status object represented by this payload.
/// </summary>
/// <remarks>
/// The value is evaluated lazily and cached.
/// </remarks>
[JsonIgnore]
public Status Status => this.status ??= this.DataResolver.GetExcelSheet<Status>().GetRow(this.statusId);
var chunkLen = idBytes.Length + 7;
var bytes = new List<byte>()
/// <inheritdoc/>
public override string ToString()
{
return $"{this.Type} - StatusId: {this.statusId}, Name: {this.Status.Name}";
}
/// <inheritdoc/>
protected override byte[] EncodeImpl()
{
var idBytes = MakeInteger(this.statusId);
var chunkLen = idBytes.Length + 7;
var bytes = new List<byte>()
{
START_BYTE, (byte)SeStringChunkType.Interactable, (byte)chunkLen, (byte)EmbeddedInfoType.Status,
};
bytes.AddRange(idBytes);
// unk
bytes.AddRange(new byte[] { 0x01, 0x01, 0xFF, 0x02, 0x20, END_BYTE });
bytes.AddRange(idBytes);
// unk
bytes.AddRange(new byte[] { 0x01, 0x01, 0xFF, 0x02, 0x20, END_BYTE });
return bytes.ToArray();
}
return bytes.ToArray();
}
/// <inheritdoc/>
protected override void DecodeImpl(BinaryReader reader, long endOfStream)
{
this.statusId = GetInteger(reader);
/// <inheritdoc/>
protected override void DecodeImpl(BinaryReader reader, long endOfStream)
{
this.statusId = GetInteger(reader);
}
}
}

View file

@ -5,97 +5,98 @@ using System.Text;
using Newtonsoft.Json;
namespace Dalamud.Game.Text.SeStringHandling.Payloads;
/// <summary>
/// An SeString Payload representing a plain text string.
/// </summary>
public class TextPayload : Payload, ITextProvider
namespace Dalamud.Game.Text.SeStringHandling.Payloads
{
[JsonProperty]
private string text;
/// <summary>
/// Initializes a new instance of the <see cref="TextPayload"/> class.
/// Creates a new TextPayload for the given text.
/// An SeString Payload representing a plain text string.
/// </summary>
/// <param name="text">The text to include for this payload.</param>
public TextPayload(string text)
public class TextPayload : Payload, ITextProvider
{
this.text = text;
}
[JsonProperty]
private string text;
/// <summary>
/// Initializes a new instance of the <see cref="TextPayload"/> class.
/// Creates a new TextPayload for the given text.
/// </summary>
internal TextPayload()
{
}
/// <inheritdoc/>
public override PayloadType Type => PayloadType.RawText;
/// <summary>
/// Gets or sets the text contained in this payload.
/// This may contain SE's special unicode characters.
/// </summary>
[JsonIgnore]
public string Text
{
get
/// <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)
{
return this.text;
this.text = text;
}
set
/// <summary>
/// Initializes a new instance of the <see cref="TextPayload"/> class.
/// Creates a new TextPayload for the given text.
/// </summary>
internal TextPayload()
{
this.text = value;
this.Dirty = true;
}
}
/// <inheritdoc/>
public override string ToString()
{
return $"{this.Type} - Text: {this.Text}";
}
/// <inheritdoc/>
protected override byte[] EncodeImpl()
{
// special case to allow for empty text payloads, so users don't have to check
// this may change or go away
if (string.IsNullOrEmpty(this.text))
{
return Array.Empty<byte>();
}
return Encoding.UTF8.GetBytes(this.text);
}
/// <inheritdoc/>
public override PayloadType Type => PayloadType.RawText;
/// <inheritdoc/>
protected override void DecodeImpl(BinaryReader reader, long endOfStream)
{
var textBytes = new List<byte>();
while (reader.BaseStream.Position < endOfStream)
/// <summary>
/// Gets or sets the text contained in this payload.
/// This may contain SE's special unicode characters.
/// </summary>
[JsonIgnore]
public string Text
{
var nextByte = reader.ReadByte();
if (nextByte == START_BYTE)
get
{
// rewind since this byte isn't part of this payload
reader.BaseStream.Position--;
break;
return this.text;
}
textBytes.Add(nextByte);
set
{
this.text = value;
this.Dirty = true;
}
}
if (textBytes.Count > 0)
/// <inheritdoc/>
public override string ToString()
{
// TODO: handling of the game's assorted special unicode characters
this.text = Encoding.UTF8.GetString(textBytes.ToArray());
return $"{this.Type} - Text: {this.Text}";
}
/// <inheritdoc/>
protected override byte[] EncodeImpl()
{
// special case to allow for empty text payloads, so users don't have to check
// this may change or go away
if (string.IsNullOrEmpty(this.text))
{
return Array.Empty<byte>();
}
return Encoding.UTF8.GetBytes(this.text);
}
/// <inheritdoc/>
protected override void DecodeImpl(BinaryReader reader, long endOfStream)
{
var textBytes = new List<byte>();
while (reader.BaseStream.Position < endOfStream)
{
var nextByte = reader.ReadByte();
if (nextByte == START_BYTE)
{
// rewind since this byte isn't part of this payload
reader.BaseStream.Position--;
break;
}
textBytes.Add(nextByte);
}
if (textBytes.Count > 0)
{
// TODO: handling of the game's assorted special unicode characters
this.text = Encoding.UTF8.GetString(textBytes.ToArray());
}
}
}
}

View file

@ -4,110 +4,111 @@ using System.IO;
using Lumina.Excel.GeneratedSheets;
using Newtonsoft.Json;
namespace Dalamud.Game.Text.SeStringHandling.Payloads;
/// <summary>
/// An SeString Payload representing a UI foreground color applied to following text payloads.
/// </summary>
public class UIForegroundPayload : Payload
namespace Dalamud.Game.Text.SeStringHandling.Payloads
{
private UIColor color;
[JsonProperty]
private ushort colorKey;
/// <summary>
/// Initializes a new instance of the <see cref="UIForegroundPayload"/> class.
/// Creates a new UIForegroundPayload for the given UIColor key.
/// An SeString Payload representing a UI foreground color applied to following text payloads.
/// </summary>
/// <param name="colorKey">A UIColor key.</param>
public UIForegroundPayload(ushort colorKey)
public class UIForegroundPayload : Payload
{
this.colorKey = colorKey;
}
private UIColor color;
/// <summary>
/// Initializes a new instance of the <see cref="UIForegroundPayload"/> class.
/// Creates a new UIForegroundPayload for the given UIColor key.
/// </summary>
internal UIForegroundPayload()
{
}
[JsonProperty]
private ushort colorKey;
/// <summary>
/// Gets a payload representing disabling foreground color on following text.
/// </summary>
// TODO Make this work with DI
public static UIForegroundPayload UIForegroundOff => new(0);
/// <inheritdoc/>
public override PayloadType Type => PayloadType.UIForeground;
/// <summary>
/// Gets a value indicating whether or not this payload represents applying a foreground color, or disabling one.
/// </summary>
public bool IsEnabled => this.ColorKey != 0;
/// <summary>
/// Gets a Lumina UIColor object representing this payload. The actual color data is at UIColor.UIForeground.
/// </summary>
/// <remarks>
/// The value is evaluated lazily and cached.
/// </remarks>
[JsonIgnore]
public UIColor UIColor => this.color ??= this.DataResolver.GetExcelSheet<UIColor>().GetRow(this.colorKey);
/// <summary>
/// Gets or sets the color key used as a lookup in the UIColor table for this foreground color.
/// </summary>
[JsonIgnore]
public ushort ColorKey
{
get
/// <summary>
/// Initializes a new instance of the <see cref="UIForegroundPayload"/> class.
/// Creates a new UIForegroundPayload for the given UIColor key.
/// </summary>
/// <param name="colorKey">A UIColor key.</param>
public UIForegroundPayload(ushort colorKey)
{
return this.colorKey;
this.colorKey = colorKey;
}
set
/// <summary>
/// Initializes a new instance of the <see cref="UIForegroundPayload"/> class.
/// Creates a new UIForegroundPayload for the given UIColor key.
/// </summary>
internal UIForegroundPayload()
{
this.colorKey = value;
this.color = null;
this.Dirty = true;
}
}
/// <summary>
/// Gets the Red/Green/Blue values for this foreground color, encoded as a typical hex color.
/// </summary>
[JsonIgnore]
public uint RGB => this.UIColor.UIForeground & 0xFFFFFF;
/// <summary>
/// Gets a payload representing disabling foreground color on following text.
/// </summary>
// TODO Make this work with DI
public static UIForegroundPayload UIForegroundOff => new(0);
/// <inheritdoc/>
public override string ToString()
{
return $"{this.Type} - UIColor: {this.colorKey} color: {(this.IsEnabled ? this.RGB : 0)}";
}
/// <inheritdoc/>
public override PayloadType Type => PayloadType.UIForeground;
/// <inheritdoc/>
protected override byte[] EncodeImpl()
{
var colorBytes = MakeInteger(this.colorKey);
var chunkLen = colorBytes.Length + 1;
/// <summary>
/// Gets a value indicating whether or not this payload represents applying a foreground color, or disabling one.
/// </summary>
public bool IsEnabled => this.ColorKey != 0;
var bytes = new List<byte>(new byte[]
/// <summary>
/// Gets a Lumina UIColor object representing this payload. The actual color data is at UIColor.UIForeground.
/// </summary>
/// <remarks>
/// The value is evaluated lazily and cached.
/// </remarks>
[JsonIgnore]
public UIColor UIColor => this.color ??= this.DataResolver.GetExcelSheet<UIColor>().GetRow(this.colorKey);
/// <summary>
/// Gets or sets the color key used as a lookup in the UIColor table for this foreground color.
/// </summary>
[JsonIgnore]
public ushort ColorKey
{
get
{
return this.colorKey;
}
set
{
this.colorKey = value;
this.color = null;
this.Dirty = true;
}
}
/// <summary>
/// Gets the Red/Green/Blue values for this foreground color, encoded as a typical hex color.
/// </summary>
[JsonIgnore]
public uint RGB => this.UIColor.UIForeground & 0xFFFFFF;
/// <inheritdoc/>
public override string ToString()
{
return $"{this.Type} - UIColor: {this.colorKey} color: {(this.IsEnabled ? this.RGB : 0)}";
}
/// <inheritdoc/>
protected override byte[] EncodeImpl()
{
var colorBytes = MakeInteger(this.colorKey);
var chunkLen = colorBytes.Length + 1;
var bytes = new List<byte>(new byte[]
{
START_BYTE, (byte)SeStringChunkType.UIForeground, (byte)chunkLen,
});
});
bytes.AddRange(colorBytes);
bytes.Add(END_BYTE);
bytes.AddRange(colorBytes);
bytes.Add(END_BYTE);
return bytes.ToArray();
}
return bytes.ToArray();
}
/// <inheritdoc/>
protected override void DecodeImpl(BinaryReader reader, long endOfStream)
{
this.colorKey = (ushort)GetInteger(reader);
/// <inheritdoc/>
protected override void DecodeImpl(BinaryReader reader, long endOfStream)
{
this.colorKey = (ushort)GetInteger(reader);
}
}
}

View file

@ -4,110 +4,111 @@ using System.IO;
using Lumina.Excel.GeneratedSheets;
using Newtonsoft.Json;
namespace Dalamud.Game.Text.SeStringHandling.Payloads;
/// <summary>
/// An SeString Payload representing a UI glow color applied to following text payloads.
/// </summary>
public class UIGlowPayload : Payload
namespace Dalamud.Game.Text.SeStringHandling.Payloads
{
private UIColor color;
[JsonProperty]
private ushort colorKey;
/// <summary>
/// Initializes a new instance of the <see cref="UIGlowPayload"/> class.
/// Creates a new UIForegroundPayload for the given UIColor key.
/// An SeString Payload representing a UI glow color applied to following text payloads.
/// </summary>
/// <param name="colorKey">A UIColor key.</param>
public UIGlowPayload(ushort colorKey)
public class UIGlowPayload : Payload
{
this.colorKey = colorKey;
}
private UIColor color;
/// <summary>
/// Initializes a new instance of the <see cref="UIGlowPayload"/> class.
/// Creates a new UIForegroundPayload for the given UIColor key.
/// </summary>
internal UIGlowPayload()
{
}
[JsonProperty]
private ushort colorKey;
/// <summary>
/// Gets a payload representing disabling glow color on following text.
/// </summary>
// TODO Make this work with DI
public static UIGlowPayload UIGlowOff => new(0);
/// <inheritdoc/>
public override PayloadType Type => PayloadType.UIGlow;
/// <summary>
/// Gets or sets the color key used as a lookup in the UIColor table for this glow color.
/// </summary>
[JsonIgnore]
public ushort ColorKey
{
get
/// <summary>
/// Initializes a new instance of the <see cref="UIGlowPayload"/> class.
/// Creates a new UIForegroundPayload for the given UIColor key.
/// </summary>
/// <param name="colorKey">A UIColor key.</param>
public UIGlowPayload(ushort colorKey)
{
return this.colorKey;
this.colorKey = colorKey;
}
set
/// <summary>
/// Initializes a new instance of the <see cref="UIGlowPayload"/> class.
/// Creates a new UIForegroundPayload for the given UIColor key.
/// </summary>
internal UIGlowPayload()
{
this.colorKey = value;
this.color = null;
this.Dirty = true;
}
}
/// <summary>
/// Gets a value indicating whether or not this payload represents applying a glow color, or disabling one.
/// </summary>
public bool IsEnabled => this.ColorKey != 0;
/// <summary>
/// Gets a payload representing disabling glow color on following text.
/// </summary>
// TODO Make this work with DI
public static UIGlowPayload UIGlowOff => new(0);
/// <summary>
/// Gets the Red/Green/Blue values for this glow color, encoded as a typical hex color.
/// </summary>
[JsonIgnore]
public uint RGB => this.UIColor.UIGlow & 0xFFFFFF;
/// <inheritdoc/>
public override PayloadType Type => PayloadType.UIGlow;
/// <summary>
/// Gets a Lumina UIColor object representing this payload. The actual color data is at UIColor.UIGlow.
/// </summary>
/// <remarks>
/// The value is evaluated lazily and cached.
/// </remarks>
[JsonIgnore]
public UIColor UIColor => this.color ??= this.DataResolver.GetExcelSheet<UIColor>().GetRow(this.colorKey);
/// <inheritdoc/>
public override string ToString()
{
return $"{this.Type} - UIColor: {this.colorKey} color: {(this.IsEnabled ? this.RGB : 0)}";
}
/// <inheritdoc/>
protected override byte[] EncodeImpl()
{
var colorBytes = MakeInteger(this.colorKey);
var chunkLen = colorBytes.Length + 1;
var bytes = new List<byte>(new byte[]
/// <summary>
/// Gets or sets the color key used as a lookup in the UIColor table for this glow color.
/// </summary>
[JsonIgnore]
public ushort ColorKey
{
get
{
return this.colorKey;
}
set
{
this.colorKey = value;
this.color = null;
this.Dirty = true;
}
}
/// <summary>
/// Gets a value indicating whether or not this payload represents applying a glow color, or disabling one.
/// </summary>
public bool IsEnabled => this.ColorKey != 0;
/// <summary>
/// Gets the Red/Green/Blue values for this glow color, encoded as a typical hex color.
/// </summary>
[JsonIgnore]
public uint RGB => this.UIColor.UIGlow & 0xFFFFFF;
/// <summary>
/// Gets a Lumina UIColor object representing this payload. The actual color data is at UIColor.UIGlow.
/// </summary>
/// <remarks>
/// The value is evaluated lazily and cached.
/// </remarks>
[JsonIgnore]
public UIColor UIColor => this.color ??= this.DataResolver.GetExcelSheet<UIColor>().GetRow(this.colorKey);
/// <inheritdoc/>
public override string ToString()
{
return $"{this.Type} - UIColor: {this.colorKey} color: {(this.IsEnabled ? this.RGB : 0)}";
}
/// <inheritdoc/>
protected override byte[] EncodeImpl()
{
var colorBytes = MakeInteger(this.colorKey);
var chunkLen = colorBytes.Length + 1;
var bytes = new List<byte>(new byte[]
{
START_BYTE, (byte)SeStringChunkType.UIGlow, (byte)chunkLen,
});
});
bytes.AddRange(colorBytes);
bytes.Add(END_BYTE);
bytes.AddRange(colorBytes);
bytes.Add(END_BYTE);
return bytes.ToArray();
}
return bytes.ToArray();
}
/// <inheritdoc/>
protected override void DecodeImpl(BinaryReader reader, long endOfStream)
{
this.colorKey = (ushort)GetInteger(reader);
/// <inheritdoc/>
protected override void DecodeImpl(BinaryReader reader, long endOfStream)
{
this.colorKey = (ushort)GetInteger(reader);
}
}
}

View file

@ -10,360 +10,361 @@ using Dalamud.Utility;
using Lumina.Excel.GeneratedSheets;
using Newtonsoft.Json;
namespace Dalamud.Game.Text.SeStringHandling;
/// <summary>
/// This class represents a parsed SeString.
/// </summary>
public class SeString
namespace Dalamud.Game.Text.SeStringHandling
{
/// <summary>
/// Initializes a new instance of the <see cref="SeString"/> class.
/// Creates a new SeString from an ordered list of payloads.
/// This class represents a parsed SeString.
/// </summary>
public SeString()
public class SeString
{
this.Payloads = new List<Payload>();
}
/// <summary>
/// Initializes a new instance of the <see cref="SeString"/> class.
/// Creates a new SeString from an ordered list of payloads.
/// </summary>
public SeString()
{
this.Payloads = new List<Payload>();
}
/// <summary>
/// Initializes a new instance of the <see cref="SeString"/> class.
/// Creates a new SeString from an ordered list of payloads.
/// </summary>
/// <param name="payloads">The Payload objects to make up this string.</param>
[JsonConstructor]
public SeString(List<Payload> payloads)
{
this.Payloads = payloads;
}
/// <summary>
/// Initializes a new instance of the <see cref="SeString"/> class.
/// Creates a new SeString from an ordered list of payloads.
/// </summary>
/// <param name="payloads">The Payload objects to make up this string.</param>
[JsonConstructor]
public SeString(List<Payload> payloads)
{
this.Payloads = payloads;
}
/// <summary>
/// Initializes a new instance of the <see cref="SeString"/> class.
/// Creates a new SeString from an ordered list of payloads.
/// </summary>
/// <param name="payloads">The Payload objects to make up this string.</param>
public SeString(params Payload[] payloads)
{
this.Payloads = new List<Payload>(payloads);
}
/// <summary>
/// Initializes a new instance of the <see cref="SeString"/> class.
/// Creates a new SeString from an ordered list of payloads.
/// </summary>
/// <param name="payloads">The Payload objects to make up this string.</param>
public SeString(params Payload[] payloads)
{
this.Payloads = new List<Payload>(payloads);
}
/// <summary>
/// Gets a list of Payloads necessary to display the arrow link marker icon in chat
/// with the appropriate glow and coloring.
/// </summary>
/// <returns>A list of all the payloads required to insert the link marker.</returns>
public static IEnumerable<Payload> TextArrowPayloads => new List<Payload>(new Payload[]
{
/// <summary>
/// Gets a list of Payloads necessary to display the arrow link marker icon in chat
/// with the appropriate glow and coloring.
/// </summary>
/// <returns>A list of all the payloads required to insert the link marker.</returns>
public static IEnumerable<Payload> TextArrowPayloads => new List<Payload>(new Payload[]
{
new UIForegroundPayload(0x01F4),
new UIGlowPayload(0x01F5),
new TextPayload($"{(char)SeIconChar.LinkMarker}"),
UIGlowPayload.UIGlowOff,
UIForegroundPayload.UIForegroundOff,
});
});
/// <summary>
/// Gets an empty SeString.
/// </summary>
public static SeString Empty => new();
/// <summary>
/// Gets an empty SeString.
/// </summary>
public static SeString Empty => new();
/// <summary>
/// Gets the ordered list of payloads included in this SeString.
/// </summary>
public List<Payload> Payloads { get; }
/// <summary>
/// Gets the ordered list of payloads included in this SeString.
/// </summary>
public List<Payload> Payloads { get; }
/// <summary>
/// Gets all of the raw text from a message as a single joined string.
/// </summary>
/// <returns>
/// All the raw text from the contained payloads, joined into a single string.
/// </returns>
public string TextValue
{
get
/// <summary>
/// Gets all of the raw text from a message as a single joined string.
/// </summary>
/// <returns>
/// All the raw text from the contained payloads, joined into a single string.
/// </returns>
public string TextValue
{
return this.Payloads
.Where(p => p is ITextProvider)
.Cast<ITextProvider>()
.Aggregate(new StringBuilder(), (sb, tp) => sb.Append(tp.Text), sb => sb.ToString());
}
}
/// <summary>
/// Implicitly convert a string into a SeString containing a <see cref="TextPayload"/>.
/// </summary>
/// <param name="str">string to convert.</param>
/// <returns>Equivalent SeString.</returns>
public static implicit operator SeString(string str) => new(new TextPayload(str));
/// <summary>
/// Implicitly convert a string into a SeString containing a <see cref="TextPayload"/>.
/// </summary>
/// <param name="str">string to convert.</param>
/// <returns>Equivalent SeString.</returns>
public static explicit operator SeString(Lumina.Text.SeString str) => str.ToDalamudString();
/// <summary>
/// Parse a binary game message into an SeString.
/// </summary>
/// <param name="ptr">Pointer to the string's data in memory.</param>
/// <param name="len">Length of the string's data in memory.</param>
/// <returns>An SeString containing parsed Payload objects for each payload in the data.</returns>
public static unsafe SeString Parse(byte* ptr, int len)
{
if (ptr == null)
return Empty;
var payloads = new List<Payload>();
using (var stream = new UnmanagedMemoryStream(ptr, len))
using (var reader = new BinaryReader(stream))
{
while (stream.Position < len)
get
{
var payload = Payload.Decode(reader);
if (payload != null)
payloads.Add(payload);
return this.Payloads
.Where(p => p is ITextProvider)
.Cast<ITextProvider>()
.Aggregate(new StringBuilder(), (sb, tp) => sb.Append(tp.Text), sb => sb.ToString());
}
}
return new SeString(payloads);
}
/// <summary>
/// Implicitly convert a string into a SeString containing a <see cref="TextPayload"/>.
/// </summary>
/// <param name="str">string to convert.</param>
/// <returns>Equivalent SeString.</returns>
public static implicit operator SeString(string str) => new(new TextPayload(str));
/// <summary>
/// Parse a binary game message into an SeString.
/// </summary>
/// <param name="data">Binary message payload data in SE's internal format.</param>
/// <returns>An SeString containing parsed Payload objects for each payload in the data.</returns>
public static unsafe SeString Parse(ReadOnlySpan<byte> data)
{
fixed (byte* ptr = data)
/// <summary>
/// Implicitly convert a string into a SeString containing a <see cref="TextPayload"/>.
/// </summary>
/// <param name="str">string to convert.</param>
/// <returns>Equivalent SeString.</returns>
public static explicit operator SeString(Lumina.Text.SeString str) => str.ToDalamudString();
/// <summary>
/// Parse a binary game message into an SeString.
/// </summary>
/// <param name="ptr">Pointer to the string's data in memory.</param>
/// <param name="len">Length of the string's data in memory.</param>
/// <returns>An SeString containing parsed Payload objects for each payload in the data.</returns>
public static unsafe SeString Parse(byte* ptr, int len)
{
return Parse(ptr, data.Length);
}
}
if (ptr == null)
return Empty;
/// <summary>
/// Parse a binary game message into an SeString.
/// </summary>
/// <param name="bytes">Binary message payload data in SE's internal format.</param>
/// <returns>An SeString containing parsed Payload objects for each payload in the data.</returns>
public static SeString Parse(byte[] bytes) => Parse(new ReadOnlySpan<byte>(bytes));
var payloads = new List<Payload>();
/// <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="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)
{
var data = Service<DataManager>.Get();
using (var stream = new UnmanagedMemoryStream(ptr, len))
using (var reader = new BinaryReader(stream))
{
while (stream.Position < len)
{
var payload = Payload.Decode(reader);
if (payload != null)
payloads.Add(payload);
}
}
var displayName = displayNameOverride ?? data.GetExcelSheet<Item>()?.GetRow(itemId)?.Name;
if (isHq)
{
displayName += $" {(char)SeIconChar.HighQuality}";
return new SeString(payloads);
}
// TODO: probably a cleaner way to build these than doing the bulk+insert
var payloads = new List<Payload>(new Payload[]
/// <summary>
/// Parse a binary game message into an SeString.
/// </summary>
/// <param name="data">Binary message payload data in SE's internal format.</param>
/// <returns>An SeString containing parsed Payload objects for each payload in the data.</returns>
public static unsafe SeString Parse(ReadOnlySpan<byte> data)
{
fixed (byte* ptr = data)
{
return Parse(ptr, data.Length);
}
}
/// <summary>
/// Parse a binary game message into an SeString.
/// </summary>
/// <param name="bytes">Binary message payload data in SE's internal format.</param>
/// <returns>An SeString containing parsed Payload objects for each payload in the data.</returns>
public static SeString Parse(byte[] bytes) => Parse(new ReadOnlySpan<byte>(bytes));
/// <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="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)
{
var data = Service<DataManager>.Get();
var displayName = displayNameOverride ?? data.GetExcelSheet<Item>()?.GetRow(itemId)?.Name;
if (isHq)
{
displayName += $" {(char)SeIconChar.HighQuality}";
}
// 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),
// arrow goes here
new TextPayload(displayName),
RawPayload.LinkTerminator,
// sometimes there is another set of uiglow/foreground off payloads here
// might be necessary when including additional text after the item name
});
payloads.InsertRange(3, TextArrowPayloads);
// sometimes there is another set of uiglow/foreground off payloads here
// might be necessary when including additional text after the item name
});
payloads.InsertRange(3, TextArrowPayloads);
return new SeString(payloads);
}
return new SeString(payloads);
}
/// <summary>
/// Creates an SeString representing an entire Payload chain that can be used to link an item in the chat log.
/// </summary>
/// <param name="item">The Lumina Item to link.</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>
/// <returns>An SeString containing all the payloads necessary to display an item link in the chat log.</returns>
public static SeString CreateItemLink(Item item, bool isHq, string? displayNameOverride = null)
{
return CreateItemLink(item.RowId, isHq, displayNameOverride ?? item.Name);
}
/// <summary>
/// Creates an SeString representing an entire Payload chain that can be used to link a map position in the chat log.
/// </summary>
/// <param name="territoryId">The id of the TerritoryType for this map link.</param>
/// <param name="mapId">The id of the Map for this map link.</param>
/// <param name="rawX">The raw x-coordinate for this link.</param>
/// <param name="rawY">The raw y-coordinate for this link..</param>
/// <returns>An SeString containing all of the payloads necessary to display a map link in the chat log.</returns>
public static SeString CreateMapLink(uint territoryId, uint mapId, int rawX, int rawY)
{
var mapPayload = new MapLinkPayload(territoryId, mapId, rawX, rawY);
var nameString = $"{mapPayload.PlaceName} {mapPayload.CoordinateString}";
var payloads = new List<Payload>(new Payload[]
/// <summary>
/// Creates an SeString representing an entire Payload chain that can be used to link an item in the chat log.
/// </summary>
/// <param name="item">The Lumina Item to link.</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>
/// <returns>An SeString containing all the payloads necessary to display an item link in the chat log.</returns>
public static SeString CreateItemLink(Item item, bool isHq, string? displayNameOverride = null)
{
mapPayload,
// arrow goes here
new TextPayload(nameString),
RawPayload.LinkTerminator,
});
payloads.InsertRange(1, TextArrowPayloads);
return CreateItemLink(item.RowId, isHq, displayNameOverride ?? item.Name);
}
return new SeString(payloads);
}
/// <summary>
/// Creates an SeString representing an entire Payload chain that can be used to link a map position in the chat log.
/// </summary>
/// <param name="territoryId">The id of the TerritoryType for this map link.</param>
/// <param name="mapId">The id of the Map for this map link.</param>
/// <param name="xCoord">The human-readable x-coordinate for this link.</param>
/// <param name="yCoord">The human-readable y-coordinate for this link.</param>
/// <param name="fudgeFactor">An optional offset to account for rounding and truncation errors; it is best to leave this untouched in most cases.</param>
/// <returns>An SeString containing all of the payloads necessary to display a map link in the chat log.</returns>
public static SeString CreateMapLink(uint territoryId, uint mapId, float xCoord, float yCoord, float fudgeFactor = 0.05f)
{
var mapPayload = new MapLinkPayload(territoryId, mapId, xCoord, yCoord, fudgeFactor);
var nameString = $"{mapPayload.PlaceName} {mapPayload.CoordinateString}";
var payloads = new List<Payload>(new Payload[]
/// <summary>
/// Creates an SeString representing an entire Payload chain that can be used to link a map position in the chat log.
/// </summary>
/// <param name="territoryId">The id of the TerritoryType for this map link.</param>
/// <param name="mapId">The id of the Map for this map link.</param>
/// <param name="rawX">The raw x-coordinate for this link.</param>
/// <param name="rawY">The raw y-coordinate for this link..</param>
/// <returns>An SeString containing all of the payloads necessary to display a map link in the chat log.</returns>
public static SeString CreateMapLink(uint territoryId, uint mapId, int rawX, int rawY)
{
mapPayload,
// arrow goes here
new TextPayload(nameString),
RawPayload.LinkTerminator,
});
payloads.InsertRange(1, TextArrowPayloads);
var mapPayload = new MapLinkPayload(territoryId, mapId, rawX, rawY);
var nameString = $"{mapPayload.PlaceName} {mapPayload.CoordinateString}";
return new SeString(payloads);
}
/// <summary>
/// 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.
/// </summary>
/// <param name="placeName">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.</param>
/// <param name="xCoord">The human-readable x-coordinate for this link.</param>
/// <param name="yCoord">The human-readable y-coordinate for this link.</param>
/// <param name="fudgeFactor">An optional offset to account for rounding and truncation errors; it is best to leave this untouched in most cases.</param>
/// <returns>An SeString containing all of the payloads necessary to display a map link in the chat log.</returns>
public static SeString? CreateMapLink(string placeName, float xCoord, float yCoord, float fudgeFactor = 0.05f)
{
var data = Service<DataManager>.Get();
var mapSheet = data.GetExcelSheet<Map>();
var matches = data.GetExcelSheet<PlaceName>()
.Where(row => row.Name.ToString().ToLowerInvariant() == placeName.ToLowerInvariant())
.ToArray();
foreach (var place in matches)
{
var map = mapSheet.FirstOrDefault(row => row.PlaceName.Row == place.RowId);
if (map != null)
var payloads = new List<Payload>(new Payload[]
{
return CreateMapLink(map.TerritoryType.Row, map.RowId, xCoord, yCoord, fudgeFactor);
mapPayload,
// arrow goes here
new TextPayload(nameString),
RawPayload.LinkTerminator,
});
payloads.InsertRange(1, TextArrowPayloads);
return new SeString(payloads);
}
/// <summary>
/// Creates an SeString representing an entire Payload chain that can be used to link a map position in the chat log.
/// </summary>
/// <param name="territoryId">The id of the TerritoryType for this map link.</param>
/// <param name="mapId">The id of the Map for this map link.</param>
/// <param name="xCoord">The human-readable x-coordinate for this link.</param>
/// <param name="yCoord">The human-readable y-coordinate for this link.</param>
/// <param name="fudgeFactor">An optional offset to account for rounding and truncation errors; it is best to leave this untouched in most cases.</param>
/// <returns>An SeString containing all of the payloads necessary to display a map link in the chat log.</returns>
public static SeString CreateMapLink(uint territoryId, uint mapId, float xCoord, float yCoord, float fudgeFactor = 0.05f)
{
var mapPayload = new MapLinkPayload(territoryId, mapId, xCoord, yCoord, fudgeFactor);
var nameString = $"{mapPayload.PlaceName} {mapPayload.CoordinateString}";
var payloads = new List<Payload>(new Payload[]
{
mapPayload,
// arrow goes here
new TextPayload(nameString),
RawPayload.LinkTerminator,
});
payloads.InsertRange(1, TextArrowPayloads);
return new SeString(payloads);
}
/// <summary>
/// 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.
/// </summary>
/// <param name="placeName">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.</param>
/// <param name="xCoord">The human-readable x-coordinate for this link.</param>
/// <param name="yCoord">The human-readable y-coordinate for this link.</param>
/// <param name="fudgeFactor">An optional offset to account for rounding and truncation errors; it is best to leave this untouched in most cases.</param>
/// <returns>An SeString containing all of the payloads necessary to display a map link in the chat log.</returns>
public static SeString? CreateMapLink(string placeName, float xCoord, float yCoord, float fudgeFactor = 0.05f)
{
var data = Service<DataManager>.Get();
var mapSheet = data.GetExcelSheet<Map>();
var matches = data.GetExcelSheet<PlaceName>()
.Where(row => row.Name.ToString().ToLowerInvariant() == placeName.ToLowerInvariant())
.ToArray();
foreach (var place in matches)
{
var map = mapSheet.FirstOrDefault(row => row.PlaceName.Row == place.RowId);
if (map != null)
{
return CreateMapLink(map.TerritoryType.Row, map.RowId, xCoord, yCoord, fudgeFactor);
}
}
// TODO: empty? throw?
return null;
}
// TODO: empty? throw?
return null;
}
/// <summary>
/// Creates a SeString from a json. (For testing - not recommended for production use.)
/// </summary>
/// <param name="json">A serialized SeString produced by ToJson() <see cref="ToJson"/>.</param>
/// <returns>A SeString initialized with values from the json.</returns>
public static SeString? FromJson(string json)
{
var s = JsonConvert.DeserializeObject<SeString>(json, new JsonSerializerSettings
/// <summary>
/// Creates a SeString from a json. (For testing - not recommended for production use.)
/// </summary>
/// <param name="json">A serialized SeString produced by ToJson() <see cref="ToJson"/>.</param>
/// <returns>A SeString initialized with values from the json.</returns>
public static SeString? FromJson(string json)
{
PreserveReferencesHandling = PreserveReferencesHandling.Objects,
TypeNameHandling = TypeNameHandling.Auto,
ConstructorHandling = ConstructorHandling.AllowNonPublicDefaultConstructor,
});
var s = JsonConvert.DeserializeObject<SeString>(json, new JsonSerializerSettings
{
PreserveReferencesHandling = PreserveReferencesHandling.Objects,
TypeNameHandling = TypeNameHandling.Auto,
ConstructorHandling = ConstructorHandling.AllowNonPublicDefaultConstructor,
});
return s;
}
/// <summary>
/// Serializes the SeString to json.
/// </summary>
/// <returns>An json representation of this object.</returns>
public string ToJson()
{
return JsonConvert.SerializeObject(this, Formatting.Indented, new JsonSerializerSettings()
{
PreserveReferencesHandling = PreserveReferencesHandling.Objects,
ReferenceLoopHandling = ReferenceLoopHandling.Ignore,
TypeNameHandling = TypeNameHandling.Auto,
});
}
/// <summary>
/// Appends the contents of one SeString to this one.
/// </summary>
/// <param name="other">The SeString to append to this one.</param>
/// <returns>This object.</returns>
public SeString Append(SeString other)
{
this.Payloads.AddRange(other.Payloads);
return this;
}
/// <summary>
/// Appends a list of payloads to this SeString.
/// </summary>
/// <param name="payloads">The Payloads to append.</param>
/// <returns>This object.</returns>
public SeString Append(List<Payload> payloads)
{
this.Payloads.AddRange(payloads);
return this;
}
/// <summary>
/// Appends a single payload to this SeString.
/// </summary>
/// <param name="payload">The payload to append.</param>
/// <returns>This object.</returns>
public SeString Append(Payload payload)
{
this.Payloads.Add(payload);
return this;
}
/// <summary>
/// Encodes the Payloads in this SeString into a binary representation
/// suitable for use by in-game handlers, such as the chat log.
/// </summary>
/// <returns>The binary encoded payload data.</returns>
public byte[] Encode()
{
var messageBytes = new List<byte>();
foreach (var p in this.Payloads)
{
messageBytes.AddRange(p.Encode());
return s;
}
return messageBytes.ToArray();
}
/// <summary>
/// Serializes the SeString to json.
/// </summary>
/// <returns>An json representation of this object.</returns>
public string ToJson()
{
return JsonConvert.SerializeObject(this, Formatting.Indented, new JsonSerializerSettings()
{
PreserveReferencesHandling = PreserveReferencesHandling.Objects,
ReferenceLoopHandling = ReferenceLoopHandling.Ignore,
TypeNameHandling = TypeNameHandling.Auto,
});
}
/// <summary>
/// Get the text value of this SeString.
/// </summary>
/// <returns>The TextValue property.</returns>
public override string ToString()
{
return this.TextValue;
/// <summary>
/// Appends the contents of one SeString to this one.
/// </summary>
/// <param name="other">The SeString to append to this one.</param>
/// <returns>This object.</returns>
public SeString Append(SeString other)
{
this.Payloads.AddRange(other.Payloads);
return this;
}
/// <summary>
/// Appends a list of payloads to this SeString.
/// </summary>
/// <param name="payloads">The Payloads to append.</param>
/// <returns>This object.</returns>
public SeString Append(List<Payload> payloads)
{
this.Payloads.AddRange(payloads);
return this;
}
/// <summary>
/// Appends a single payload to this SeString.
/// </summary>
/// <param name="payload">The payload to append.</param>
/// <returns>This object.</returns>
public SeString Append(Payload payload)
{
this.Payloads.Add(payload);
return this;
}
/// <summary>
/// Encodes the Payloads in this SeString into a binary representation
/// suitable for use by in-game handlers, such as the chat log.
/// </summary>
/// <returns>The binary encoded payload data.</returns>
public byte[] Encode()
{
var messageBytes = new List<byte>();
foreach (var p in this.Payloads)
{
messageBytes.AddRange(p.Encode());
}
return messageBytes.ToArray();
}
/// <summary>
/// Get the text value of this SeString.
/// </summary>
/// <returns>The TextValue property.</returns>
public override string ToString()
{
return this.TextValue;
}
}
}

View file

@ -9,108 +9,109 @@ using Dalamud.IoC;
using Dalamud.IoC.Internal;
using Lumina.Excel.GeneratedSheets;
namespace Dalamud.Game.Text.SeStringHandling;
/// <summary>
/// This class facilitates creating new SeStrings and breaking down existing ones into their individual payload components.
/// </summary>
[PluginInterface]
[InterfaceVersion("1.0")]
[Obsolete("This class is obsolete. Please use the static methods on SeString instead.")]
public sealed class SeStringManager
namespace Dalamud.Game.Text.SeStringHandling
{
/// <summary>
/// Initializes a new instance of the <see cref="SeStringManager"/> class.
/// This class facilitates creating new SeStrings and breaking down existing ones into their individual payload components.
/// </summary>
internal SeStringManager()
[PluginInterface]
[InterfaceVersion("1.0")]
[Obsolete("This class is obsolete. Please use the static methods on SeString instead.")]
public sealed class SeStringManager
{
/// <summary>
/// Initializes a new instance of the <see cref="SeStringManager"/> class.
/// </summary>
internal SeStringManager()
{
}
/// <summary>
/// Parse a binary game message into an SeString.
/// </summary>
/// <param name="ptr">Pointer to the string's data in memory.</param>
/// <param name="len">Length of the string's data in memory.</param>
/// <returns>An SeString containing parsed Payload objects for each payload in the data.</returns>
[Obsolete("This method is obsolete. Please use the static methods on SeString instead.", true)]
public unsafe SeString Parse(byte* ptr, int len) => SeString.Parse(ptr, len);
/// <summary>
/// Parse a binary game message into an SeString.
/// </summary>
/// <param name="data">Binary message payload data in SE's internal format.</param>
/// <returns>An SeString containing parsed Payload objects for each payload in the data.</returns>
[Obsolete("This method is obsolete. Please use the static methods on SeString instead.", true)]
public unsafe SeString Parse(ReadOnlySpan<byte> data) => SeString.Parse(data);
/// <summary>
/// Parse a binary game message into an SeString.
/// </summary>
/// <param name="bytes">Binary message payload data in SE's internal format.</param>
/// <returns>An SeString containing parsed Payload objects for each payload in the data.</returns>
[Obsolete("This method is obsolete. Please use the static methods on SeString instead.", true)]
public SeString Parse(byte[] bytes) => SeString.Parse(new ReadOnlySpan<byte>(bytes));
/// <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="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>
[Obsolete("This method is obsolete. Please use the static methods on SeString instead.", true)]
public SeString CreateItemLink(uint itemId, bool isHQ, string displayNameOverride = null) => SeString.CreateItemLink(itemId, isHQ, 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="item">The Lumina Item to link.</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>
/// <returns>An SeString containing all the payloads necessary to display an item link in the chat log.</returns>
[Obsolete("This method is obsolete. Please use the static methods on SeString instead.", true)]
public SeString CreateItemLink(Item item, bool isHQ, string displayNameOverride = null) => SeString.CreateItemLink(item, isHQ, displayNameOverride);
/// <summary>
/// Creates an SeString representing an entire Payload chain that can be used to link a map position in the chat log.
/// </summary>
/// <param name="territoryId">The id of the TerritoryType for this map link.</param>
/// <param name="mapId">The id of the Map for this map link.</param>
/// <param name="rawX">The raw x-coordinate for this link.</param>
/// <param name="rawY">The raw y-coordinate for this link..</param>
/// <returns>An SeString containing all of the payloads necessary to display a map link in the chat log.</returns>
[Obsolete("This method is obsolete. Please use the static methods on SeString instead.", true)]
public SeString CreateMapLink(uint territoryId, uint mapId, int rawX, int rawY) =>
SeString.CreateMapLink(territoryId, mapId, rawX, rawY);
/// <summary>
/// Creates an SeString representing an entire Payload chain that can be used to link a map position in the chat log.
/// </summary>
/// <param name="territoryId">The id of the TerritoryType for this map link.</param>
/// <param name="mapId">The id of the Map for this map link.</param>
/// <param name="xCoord">The human-readable x-coordinate for this link.</param>
/// <param name="yCoord">The human-readable y-coordinate for this link.</param>
/// <param name="fudgeFactor">An optional offset to account for rounding and truncation errors; it is best to leave this untouched in most cases.</param>
/// <returns>An SeString containing all of the payloads necessary to display a map link in the chat log.</returns>
[Obsolete("This method is obsolete. Please use the static methods on SeString instead.", true)]
public SeString CreateMapLink(uint territoryId, uint mapId, float xCoord, float yCoord, float fudgeFactor = 0.05f) => SeString.CreateMapLink(territoryId, mapId, xCoord, yCoord, fudgeFactor);
/// <summary>
/// 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.
/// </summary>
/// <param name="placeName">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.</param>
/// <param name="xCoord">The human-readable x-coordinate for this link.</param>
/// <param name="yCoord">The human-readable y-coordinate for this link.</param>
/// <param name="fudgeFactor">An optional offset to account for rounding and truncation errors; it is best to leave this untouched in most cases.</param>
/// <returns>An SeString containing all of the payloads necessary to display a map link in the chat log.</returns>
[Obsolete("This method is obsolete. Please use the static methods on SeString instead.", true)]
public SeString CreateMapLink(string placeName, float xCoord, float yCoord, float fudgeFactor = 0.05f) => SeString.CreateMapLink(placeName, xCoord, yCoord, fudgeFactor);
/// <summary>
/// Creates a list of Payloads necessary to display the arrow link marker icon in chat
/// with the appropriate glow and coloring.
/// </summary>
/// <returns>A list of all the payloads required to insert the link marker.</returns>
[Obsolete("This data is obsolete. Please use the static version on SeString instead.", true)]
public List<Payload> TextArrowPayloads() => new(SeString.TextArrowPayloads);
}
/// <summary>
/// Parse a binary game message into an SeString.
/// </summary>
/// <param name="ptr">Pointer to the string's data in memory.</param>
/// <param name="len">Length of the string's data in memory.</param>
/// <returns>An SeString containing parsed Payload objects for each payload in the data.</returns>
[Obsolete("This method is obsolete. Please use the static methods on SeString instead.", true)]
public unsafe SeString Parse(byte* ptr, int len) => SeString.Parse(ptr, len);
/// <summary>
/// Parse a binary game message into an SeString.
/// </summary>
/// <param name="data">Binary message payload data in SE's internal format.</param>
/// <returns>An SeString containing parsed Payload objects for each payload in the data.</returns>
[Obsolete("This method is obsolete. Please use the static methods on SeString instead.", true)]
public unsafe SeString Parse(ReadOnlySpan<byte> data) => SeString.Parse(data);
/// <summary>
/// Parse a binary game message into an SeString.
/// </summary>
/// <param name="bytes">Binary message payload data in SE's internal format.</param>
/// <returns>An SeString containing parsed Payload objects for each payload in the data.</returns>
[Obsolete("This method is obsolete. Please use the static methods on SeString instead.", true)]
public SeString Parse(byte[] bytes) => SeString.Parse(new ReadOnlySpan<byte>(bytes));
/// <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="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>
[Obsolete("This method is obsolete. Please use the static methods on SeString instead.", true)]
public SeString CreateItemLink(uint itemId, bool isHQ, string displayNameOverride = null) => SeString.CreateItemLink(itemId, isHQ, 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="item">The Lumina Item to link.</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>
/// <returns>An SeString containing all the payloads necessary to display an item link in the chat log.</returns>
[Obsolete("This method is obsolete. Please use the static methods on SeString instead.", true)]
public SeString CreateItemLink(Item item, bool isHQ, string displayNameOverride = null) => SeString.CreateItemLink(item, isHQ, displayNameOverride);
/// <summary>
/// Creates an SeString representing an entire Payload chain that can be used to link a map position in the chat log.
/// </summary>
/// <param name="territoryId">The id of the TerritoryType for this map link.</param>
/// <param name="mapId">The id of the Map for this map link.</param>
/// <param name="rawX">The raw x-coordinate for this link.</param>
/// <param name="rawY">The raw y-coordinate for this link..</param>
/// <returns>An SeString containing all of the payloads necessary to display a map link in the chat log.</returns>
[Obsolete("This method is obsolete. Please use the static methods on SeString instead.", true)]
public SeString CreateMapLink(uint territoryId, uint mapId, int rawX, int rawY) =>
SeString.CreateMapLink(territoryId, mapId, rawX, rawY);
/// <summary>
/// Creates an SeString representing an entire Payload chain that can be used to link a map position in the chat log.
/// </summary>
/// <param name="territoryId">The id of the TerritoryType for this map link.</param>
/// <param name="mapId">The id of the Map for this map link.</param>
/// <param name="xCoord">The human-readable x-coordinate for this link.</param>
/// <param name="yCoord">The human-readable y-coordinate for this link.</param>
/// <param name="fudgeFactor">An optional offset to account for rounding and truncation errors; it is best to leave this untouched in most cases.</param>
/// <returns>An SeString containing all of the payloads necessary to display a map link in the chat log.</returns>
[Obsolete("This method is obsolete. Please use the static methods on SeString instead.", true)]
public SeString CreateMapLink(uint territoryId, uint mapId, float xCoord, float yCoord, float fudgeFactor = 0.05f) => SeString.CreateMapLink(territoryId, mapId, xCoord, yCoord, fudgeFactor);
/// <summary>
/// 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.
/// </summary>
/// <param name="placeName">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.</param>
/// <param name="xCoord">The human-readable x-coordinate for this link.</param>
/// <param name="yCoord">The human-readable y-coordinate for this link.</param>
/// <param name="fudgeFactor">An optional offset to account for rounding and truncation errors; it is best to leave this untouched in most cases.</param>
/// <returns>An SeString containing all of the payloads necessary to display a map link in the chat log.</returns>
[Obsolete("This method is obsolete. Please use the static methods on SeString instead.", true)]
public SeString CreateMapLink(string placeName, float xCoord, float yCoord, float fudgeFactor = 0.05f) => SeString.CreateMapLink(placeName, xCoord, yCoord, fudgeFactor);
/// <summary>
/// Creates a list of Payloads necessary to display the arrow link marker icon in chat
/// with the appropriate glow and coloring.
/// </summary>
/// <returns>A list of all the payloads required to insert the link marker.</returns>
[Obsolete("This data is obsolete. Please use the static version on SeString instead.", true)]
public List<Payload> TextArrowPayloads() => new(SeString.TextArrowPayloads);
}

View file

@ -2,35 +2,36 @@ using System;
using Dalamud.Game.Text.SeStringHandling;
namespace Dalamud.Game.Text;
/// <summary>
/// This class represents a single chat log entry.
/// </summary>
public sealed class XivChatEntry
namespace Dalamud.Game.Text
{
/// <summary>
/// Gets or sets the type of entry.
/// This class represents a single chat log entry.
/// </summary>
public XivChatType Type { get; set; } = XivChatType.Debug;
public sealed class XivChatEntry
{
/// <summary>
/// Gets or sets the type of entry.
/// </summary>
public XivChatType Type { get; set; } = XivChatType.Debug;
/// <summary>
/// Gets or sets the sender ID.
/// </summary>
public uint SenderId { get; set; }
/// <summary>
/// Gets or sets the sender ID.
/// </summary>
public uint SenderId { get; set; }
/// <summary>
/// Gets or sets the sender name.
/// </summary>
public SeString Name { get; set; } = string.Empty;
/// <summary>
/// Gets or sets the sender name.
/// </summary>
public SeString Name { get; set; } = string.Empty;
/// <summary>
/// Gets or sets the message.
/// </summary>
public SeString Message { get; set; } = string.Empty;
/// <summary>
/// Gets or sets the message.
/// </summary>
public SeString Message { get; set; } = string.Empty;
/// <summary>
/// Gets or sets the message parameters.
/// </summary>
public IntPtr Parameters { get; set; }
/// <summary>
/// Gets or sets the message parameters.
/// </summary>
public IntPtr Parameters { get; set; }
}
}

View file

@ -1,236 +1,237 @@
namespace Dalamud.Game.Text;
/// <summary>
/// The FFXIV chat types as seen in the LogKind ex table.
/// </summary>
public enum XivChatType : ushort // FIXME: this is a single byte
namespace Dalamud.Game.Text
{
/// <summary>
/// No chat type.
/// The FFXIV chat types as seen in the LogKind ex table.
/// </summary>
None = 0,
public enum XivChatType : ushort // FIXME: this is a single byte
{
/// <summary>
/// No chat type.
/// </summary>
None = 0,
/// <summary>
/// The debug chat type.
/// </summary>
Debug = 1,
/// <summary>
/// The debug chat type.
/// </summary>
Debug = 1,
/// <summary>
/// The urgent chat type.
/// </summary>
[XivChatTypeInfo("Urgent", "urgent", 0xFF9400D3)]
Urgent = 2,
/// <summary>
/// The urgent chat type.
/// </summary>
[XivChatTypeInfo("Urgent", "urgent", 0xFF9400D3)]
Urgent = 2,
/// <summary>
/// The notice chat type.
/// </summary>
[XivChatTypeInfo("Notice", "notice", 0xFF9400D3)]
Notice = 3,
/// <summary>
/// The notice chat type.
/// </summary>
[XivChatTypeInfo("Notice", "notice", 0xFF9400D3)]
Notice = 3,
/// <summary>
/// The say chat type.
/// </summary>
[XivChatTypeInfo("Say", "say", 0xFFFFFFFF)]
Say = 10,
/// <summary>
/// The say chat type.
/// </summary>
[XivChatTypeInfo("Say", "say", 0xFFFFFFFF)]
Say = 10,
/// <summary>
/// The shout chat type.
/// </summary>
[XivChatTypeInfo("Shout", "shout", 0xFFFF4500)]
Shout = 11,
/// <summary>
/// The shout chat type.
/// </summary>
[XivChatTypeInfo("Shout", "shout", 0xFFFF4500)]
Shout = 11,
/// <summary>
/// The outgoing tell chat type.
/// </summary>
TellOutgoing = 12,
/// <summary>
/// The outgoing tell chat type.
/// </summary>
TellOutgoing = 12,
/// <summary>
/// The incoming tell chat type.
/// </summary>
[XivChatTypeInfo("Tell", "tell", 0xFFFF69B4)]
TellIncoming = 13,
/// <summary>
/// The incoming tell chat type.
/// </summary>
[XivChatTypeInfo("Tell", "tell", 0xFFFF69B4)]
TellIncoming = 13,
/// <summary>
/// The party chat type.
/// </summary>
[XivChatTypeInfo("Party", "party", 0xFF1E90FF)]
Party = 14,
/// <summary>
/// The party chat type.
/// </summary>
[XivChatTypeInfo("Party", "party", 0xFF1E90FF)]
Party = 14,
/// <summary>
/// The alliance chat type.
/// </summary>
[XivChatTypeInfo("Alliance", "alliance", 0xFFFF4500)]
Alliance = 15,
/// <summary>
/// The alliance chat type.
/// </summary>
[XivChatTypeInfo("Alliance", "alliance", 0xFFFF4500)]
Alliance = 15,
/// <summary>
/// The linkshell 1 chat type.
/// </summary>
[XivChatTypeInfo("Linkshell 1", "ls1", 0xFF228B22)]
Ls1 = 16,
/// <summary>
/// The linkshell 1 chat type.
/// </summary>
[XivChatTypeInfo("Linkshell 1", "ls1", 0xFF228B22)]
Ls1 = 16,
/// <summary>
/// The linkshell 2 chat type.
/// </summary>
[XivChatTypeInfo("Linkshell 2", "ls2", 0xFF228B22)]
Ls2 = 17,
/// <summary>
/// The linkshell 2 chat type.
/// </summary>
[XivChatTypeInfo("Linkshell 2", "ls2", 0xFF228B22)]
Ls2 = 17,
/// <summary>
/// The linkshell 3 chat type.
/// </summary>
[XivChatTypeInfo("Linkshell 3", "ls3", 0xFF228B22)]
Ls3 = 18,
/// <summary>
/// The linkshell 3 chat type.
/// </summary>
[XivChatTypeInfo("Linkshell 3", "ls3", 0xFF228B22)]
Ls3 = 18,
/// <summary>
/// The linkshell 4 chat type.
/// </summary>
[XivChatTypeInfo("Linkshell 4", "ls4", 0xFF228B22)]
Ls4 = 19,
/// <summary>
/// The linkshell 4 chat type.
/// </summary>
[XivChatTypeInfo("Linkshell 4", "ls4", 0xFF228B22)]
Ls4 = 19,
/// <summary>
/// The linkshell 5 chat type.
/// </summary>
[XivChatTypeInfo("Linkshell 5", "ls5", 0xFF228B22)]
Ls5 = 20,
/// <summary>
/// The linkshell 5 chat type.
/// </summary>
[XivChatTypeInfo("Linkshell 5", "ls5", 0xFF228B22)]
Ls5 = 20,
/// <summary>
/// The linkshell 6 chat type.
/// </summary>
[XivChatTypeInfo("Linkshell 6", "ls6", 0xFF228B22)]
Ls6 = 21,
/// <summary>
/// The linkshell 6 chat type.
/// </summary>
[XivChatTypeInfo("Linkshell 6", "ls6", 0xFF228B22)]
Ls6 = 21,
/// <summary>
/// The linkshell 7 chat type.
/// </summary>
[XivChatTypeInfo("Linkshell 7", "ls7", 0xFF228B22)]
Ls7 = 22,
/// <summary>
/// The linkshell 7 chat type.
/// </summary>
[XivChatTypeInfo("Linkshell 7", "ls7", 0xFF228B22)]
Ls7 = 22,
/// <summary>
/// The linkshell 8 chat type.
/// </summary>
[XivChatTypeInfo("Linkshell 8", "ls8", 0xFF228B22)]
Ls8 = 23,
/// <summary>
/// The linkshell 8 chat type.
/// </summary>
[XivChatTypeInfo("Linkshell 8", "ls8", 0xFF228B22)]
Ls8 = 23,
/// <summary>
/// The free company chat type.
/// </summary>
[XivChatTypeInfo("Free Company", "fc", 0xFF00BFFF)]
FreeCompany = 24,
/// <summary>
/// The free company chat type.
/// </summary>
[XivChatTypeInfo("Free Company", "fc", 0xFF00BFFF)]
FreeCompany = 24,
/// <summary>
/// The novice network chat type.
/// </summary>
[XivChatTypeInfo("Novice Network", "nn", 0xFF8B4513)]
NoviceNetwork = 27,
/// <summary>
/// The novice network chat type.
/// </summary>
[XivChatTypeInfo("Novice Network", "nn", 0xFF8B4513)]
NoviceNetwork = 27,
/// <summary>
/// The custom emotes chat type.
/// </summary>
[XivChatTypeInfo("Custom Emotes", "emote", 0xFF8B4513)]
CustomEmote = 28,
/// <summary>
/// The custom emotes chat type.
/// </summary>
[XivChatTypeInfo("Custom Emotes", "emote", 0xFF8B4513)]
CustomEmote = 28,
/// <summary>
/// The standard emotes chat type.
/// </summary>
[XivChatTypeInfo("Standard Emotes", "emote", 0xFF8B4513)]
StandardEmote = 29,
/// <summary>
/// The standard emotes chat type.
/// </summary>
[XivChatTypeInfo("Standard Emotes", "emote", 0xFF8B4513)]
StandardEmote = 29,
/// <summary>
/// The yell chat type.
/// </summary>
[XivChatTypeInfo("Yell", "yell", 0xFFFFFF00)]
Yell = 30,
/// <summary>
/// The yell chat type.
/// </summary>
[XivChatTypeInfo("Yell", "yell", 0xFFFFFF00)]
Yell = 30,
/// <summary>
/// The cross-world party chat type.
/// </summary>
[XivChatTypeInfo("Party", "party", 0xFF1E90FF)]
CrossParty = 32,
/// <summary>
/// The cross-world party chat type.
/// </summary>
[XivChatTypeInfo("Party", "party", 0xFF1E90FF)]
CrossParty = 32,
/// <summary>
/// The PvP team chat type.
/// </summary>
[XivChatTypeInfo("PvP Team", "pvpt", 0xFFF4A460)]
PvPTeam = 36,
/// <summary>
/// The PvP team chat type.
/// </summary>
[XivChatTypeInfo("PvP Team", "pvpt", 0xFFF4A460)]
PvPTeam = 36,
/// <summary>
/// The cross-world linkshell chat type.
/// </summary>
[XivChatTypeInfo("Crossworld Linkshell 1", "cw1", 0xFF1E90FF)]
CrossLinkShell1 = 37,
/// <summary>
/// The cross-world linkshell chat type.
/// </summary>
[XivChatTypeInfo("Crossworld Linkshell 1", "cw1", 0xFF1E90FF)]
CrossLinkShell1 = 37,
/// <summary>
/// The echo chat type.
/// </summary>
[XivChatTypeInfo("Echo", "echo", 0xFF808080)]
Echo = 56,
/// <summary>
/// The echo chat type.
/// </summary>
[XivChatTypeInfo("Echo", "echo", 0xFF808080)]
Echo = 56,
/// <summary>
/// The system error chat type.
/// </summary>
SystemError = 58,
/// <summary>
/// The system error chat type.
/// </summary>
SystemError = 58,
/// <summary>
/// The system message chat type.
/// </summary>
SystemMessage = 57,
/// <summary>
/// The system message chat type.
/// </summary>
SystemMessage = 57,
/// <summary>
/// The system message (gathering) chat type.
/// </summary>
GatheringSystemMessage = 59,
/// <summary>
/// The system message (gathering) chat type.
/// </summary>
GatheringSystemMessage = 59,
/// <summary>
/// The error message chat type.
/// </summary>
ErrorMessage = 60,
/// <summary>
/// The error message chat type.
/// </summary>
ErrorMessage = 60,
/// <summary>
/// The retainer sale chat type.
/// </summary>
/// <remarks>
/// This might be used for other purposes.
/// </remarks>
RetainerSale = 71,
/// <summary>
/// The retainer sale chat type.
/// </summary>
/// <remarks>
/// This might be used for other purposes.
/// </remarks>
RetainerSale = 71,
/// <summary>
/// The cross-world linkshell 2 chat type.
/// </summary>
[XivChatTypeInfo("Crossworld Linkshell 2", "cw2", 0xFF1E90FF)]
CrossLinkShell2 = 101,
/// <summary>
/// The cross-world linkshell 2 chat type.
/// </summary>
[XivChatTypeInfo("Crossworld Linkshell 2", "cw2", 0xFF1E90FF)]
CrossLinkShell2 = 101,
/// <summary>
/// The cross-world linkshell 3 chat type.
/// </summary>
[XivChatTypeInfo("Crossworld Linkshell 3", "cw3", 0xFF1E90FF)]
CrossLinkShell3 = 102,
/// <summary>
/// The cross-world linkshell 3 chat type.
/// </summary>
[XivChatTypeInfo("Crossworld Linkshell 3", "cw3", 0xFF1E90FF)]
CrossLinkShell3 = 102,
/// <summary>
/// The cross-world linkshell 4 chat type.
/// </summary>
[XivChatTypeInfo("Crossworld Linkshell 4", "cw4", 0xFF1E90FF)]
CrossLinkShell4 = 103,
/// <summary>
/// The cross-world linkshell 4 chat type.
/// </summary>
[XivChatTypeInfo("Crossworld Linkshell 4", "cw4", 0xFF1E90FF)]
CrossLinkShell4 = 103,
/// <summary>
/// The cross-world linkshell 5 chat type.
/// </summary>
[XivChatTypeInfo("Crossworld Linkshell 5", "cw5", 0xFF1E90FF)]
CrossLinkShell5 = 104,
/// <summary>
/// The cross-world linkshell 5 chat type.
/// </summary>
[XivChatTypeInfo("Crossworld Linkshell 5", "cw5", 0xFF1E90FF)]
CrossLinkShell5 = 104,
/// <summary>
/// The cross-world linkshell 6 chat type.
/// </summary>
[XivChatTypeInfo("Crossworld Linkshell 6", "cw6", 0xFF1E90FF)]
CrossLinkShell6 = 105,
/// <summary>
/// The cross-world linkshell 6 chat type.
/// </summary>
[XivChatTypeInfo("Crossworld Linkshell 6", "cw6", 0xFF1E90FF)]
CrossLinkShell6 = 105,
/// <summary>
/// The cross-world linkshell 7 chat type.
/// </summary>
[XivChatTypeInfo("Crossworld Linkshell 7", "cw7", 0xFF1E90FF)]
CrossLinkShell7 = 106,
/// <summary>
/// The cross-world linkshell 7 chat type.
/// </summary>
[XivChatTypeInfo("Crossworld Linkshell 7", "cw7", 0xFF1E90FF)]
CrossLinkShell7 = 106,
/// <summary>
/// The cross-world linkshell 8 chat type.
/// </summary>
[XivChatTypeInfo("Crossworld Linkshell 8", "cw8", 0xFF1E90FF)]
CrossLinkShell8 = 107,
/// <summary>
/// The cross-world linkshell 8 chat type.
/// </summary>
[XivChatTypeInfo("Crossworld Linkshell 8", "cw8", 0xFF1E90FF)]
CrossLinkShell8 = 107,
}
}

View file

@ -1,19 +1,20 @@
using Dalamud.Utility;
namespace Dalamud.Game.Text;
/// <summary>
/// Extension methods for the <see cref="XivChatType"/> type.
/// </summary>
public static class XivChatTypeExtensions
namespace Dalamud.Game.Text
{
/// <summary>
/// Get the InfoAttribute associated with this chat type.
/// Extension methods for the <see cref="XivChatType"/> type.
/// </summary>
/// <param name="chatType">The chat type.</param>
/// <returns>The info attribute.</returns>
public static XivChatTypeInfoAttribute GetDetails(this XivChatType chatType)
public static class XivChatTypeExtensions
{
return chatType.GetAttribute<XivChatTypeInfoAttribute>();
/// <summary>
/// Get the InfoAttribute associated with this chat type.
/// </summary>
/// <param name="chatType">The chat type.</param>
/// <returns>The info attribute.</returns>
public static XivChatTypeInfoAttribute GetDetails(this XivChatType chatType)
{
return chatType.GetAttribute<XivChatTypeInfoAttribute>();
}
}
}

View file

@ -1,38 +1,39 @@
using System;
namespace Dalamud.Game.Text;
/// <summary>
/// Storage for relevant information associated with the chat type.
/// </summary>
[AttributeUsage(AttributeTargets.Field)]
public class XivChatTypeInfoAttribute : Attribute
namespace Dalamud.Game.Text
{
/// <summary>
/// Initializes a new instance of the <see cref="XivChatTypeInfoAttribute"/> class.
/// Storage for relevant information associated with the chat type.
/// </summary>
/// <param name="fancyName">The fancy name.</param>
/// <param name="slug">The name slug.</param>
/// <param name="defaultColor">The default color.</param>
internal XivChatTypeInfoAttribute(string fancyName, string slug, uint defaultColor)
[AttributeUsage(AttributeTargets.Field)]
public class XivChatTypeInfoAttribute : Attribute
{
this.FancyName = fancyName;
this.Slug = slug;
this.DefaultColor = defaultColor;
/// <summary>
/// Initializes a new instance of the <see cref="XivChatTypeInfoAttribute"/> class.
/// </summary>
/// <param name="fancyName">The fancy name.</param>
/// <param name="slug">The name slug.</param>
/// <param name="defaultColor">The default color.</param>
internal XivChatTypeInfoAttribute(string fancyName, string slug, uint defaultColor)
{
this.FancyName = fancyName;
this.Slug = slug;
this.DefaultColor = defaultColor;
}
/// <summary>
/// Gets the "fancy" name of the type.
/// </summary>
public string FancyName { get; }
/// <summary>
/// Gets the type name slug or short-form.
/// </summary>
public string Slug { get; }
/// <summary>
/// Gets the type default color.
/// </summary>
public uint DefaultColor { get; }
}
/// <summary>
/// Gets the "fancy" name of the type.
/// </summary>
public string FancyName { get; }
/// <summary>
/// Gets the type name slug or short-form.
/// </summary>
public string Slug { get; }
/// <summary>
/// Gets the type default color.
/// </summary>
public uint DefaultColor { get; }
}