StyleCop: everything else

This commit is contained in:
Raymond Lynch 2021-05-30 07:10:00 -04:00
parent f64c9b8321
commit 595fd3f1e4
134 changed files with 16346 additions and 6202 deletions

View file

@ -1,120 +1,438 @@
#pragma warning disable 1591
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
{
/// <summary>
/// No icon.
/// </summary>
None = 0,
namespace Dalamud.Game.Text.SeStringHandling {
public enum BitmapFontIcon : uint {
None,
ControllerDPadUp,
ControllerDPadDown,
ControllerDPadLeft,
ControllerDPadRight,
ControllerDPadUpDown,
ControllerDPadLeftRight,
ControllerDPadAll,
/// <summary>
/// The controller D-pad up icon.
/// </summary>
ControllerDPadUp = 1,
ControllerButton0, // Xbox B / PS Circle
ControllerButton1, // Xbox A / PS Cross
ControllerButton2, // Xbox X / PS Square
ControllerButton3, // Xbox Y / PS Triangle
/// <summary>
/// The controller D-pad down icon.
/// </summary>
ControllerDPadDown = 2,
ControllerShoulderLeft,
ControllerShoulderRight,
/// <summary>
/// The controller D-pad left icon.
/// </summary>
ControllerDPadLeft = 3,
ControllerTriggerLeft,
ControllerTriggerRight,
/// <summary>
/// The controller D-pad right icon.
/// </summary>
ControllerDPadRight = 4,
ControllerAnalogLeftStickIn,
ControllerAnalogRightStickIn,
/// <summary>
/// The controller D-pad up/down icon.
/// </summary>
ControllerDPadUpDown = 5,
ControllerStart,
ControllerBack,
/// <summary>
/// The controller D-pad left/right icon.
/// </summary>
ControllerDPadLeftRight = 6,
ControllerAnalogLeftStick,
ControllerAnalogLeftStickUpDown,
ControllerAnalogLeftStickLeftRight,
/// <summary>
/// The controller D-pad all directions icon.
/// </summary>
ControllerDPadAll = 7,
ControllerAnalogRightStick,
ControllerAnalogRightStickUpDown,
ControllerAnalogRightStickLeftRight,
/// <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,
BlackShroud,
Thanalan,
AutoTranslateBegin,
AutoTranslateEnd,
ElementFire,
ElementIce,
ElementWind,
ElementEarth,
ElementLightning,
ElementWater,
LevelSync,
Warning,
Ishgard,
Aetheryte,
Aethernet,
GoldStar,
SilverStar,
/// <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,
SwordUnsheathed,
SwordSheathed,
Dice,
/// <summary>
/// The unsheathed sword icon.
/// </summary>
SwordUnsheathed = 71,
FlyZone,
FlyZoneLocked,
/// <summary>
/// The sheathed sword icon.
/// </summary>
SwordSheathed = 72,
NoCircle,
/// <summary>
/// The dice icon.
/// </summary>
Dice = 73,
NewAdventurer,
Mentor,
MentorPvE,
MentorCrafting,
MentorPvP,
/// <summary>
/// The flyable zone icon.
/// </summary>
FlyZone = 74,
Tank,
Healer,
DPS,
Crafter,
Gatherer,
AnyClass,
/// <summary>
/// The no-flying zone icon.
/// </summary>
FlyZoneLocked = 75,
CrossWorld,
/// <summary>
/// The no-circle/prohibited icon.
/// </summary>
NoCircle = 76,
FateSlay,
FateBoss,
FateGather,
FateDefend,
FateEscort,
FateSpecial1,
/// <summary>
/// The sprout icon.
/// </summary>
NewAdventurer = 77,
Returner,
/// <summary>
/// The mentor icon.
/// </summary>
Mentor = 78,
FarEast,
GyrAbania,
/// <summary>
/// The PvE mentor icon.
/// </summary>
MentorPvE = 79,
FateSpecial2,
/// <summary>
/// The crafting mentor icon.
/// </summary>
MentorCrafting = 80,
PriorityWorld,
/// <summary>
/// The PvP mentor icon.
/// </summary>
MentorPvP = 81,
ElementalLevel,
ExclamationRectangle,
/// <summary>
/// The tank role icon.
/// </summary>
Tank = 82,
NotoriousMonster,
/// <summary>
/// The healer role icon.
/// </summary>
Healer = 83,
Recording,
Alarm,
ArrowUp,
ArrowDown,
Crystarium,
MentorProblem,
/// <summary>
/// The DPS role icon.
/// </summary>
DPS = 84,
FateUnknownGold,
/// <summary>
/// The crafter role icon.
/// </summary>
Crafter = 85,
OrangeDiamond,
FateCrafting
/// <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,9 +1,13 @@
using System;
namespace Dalamud.Game.Text.SeStringHandling
{
/// <summary>
/// An interface binding for a payload that can provide readable Text.
/// </summary>
public interface ITextProvider
{
/// <summary>
/// Gets the readable text.
/// </summary>
string Text { get; }
}
}

View file

@ -1,7 +1,7 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using Dalamud.Data;
using Dalamud.Game.Text.SeStringHandling.Payloads;
using Serilog;
@ -19,35 +19,8 @@ namespace Dalamud.Game.Text.SeStringHandling
/// <summary>
/// This class represents a parsed SeString payload.
/// </summary>
public abstract class Payload
public abstract partial class Payload
{
/// <summary>
/// The type of this payload.
/// </summary>
public abstract PayloadType Type { get; }
/// <summary>
/// Whether this payload has been modified since the last Encode().
/// </summary>
public bool Dirty { get; protected set; } = true;
/// <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();
// 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
/// <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>
protected abstract void DecodeImpl(BinaryReader reader, long endOfStream);
/// <summary>
/// The Lumina instance to use for any necessary data lookups.
/// </summary>
@ -58,31 +31,26 @@ namespace Dalamud.Game.Text.SeStringHandling
private byte[] encodedData;
/// <summary>
/// Encode this payload object into a byte[] useable in-game for things like the chat log.
/// Gets the type of this payload.
/// </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 (Dirty || force)
{
this.encodedData = EncodeImpl();
Dirty = false;
}
public abstract PayloadType Type { get; }
return this.encodedData;
}
/// <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>
/// <param name="data">The DataManager instance.</param>
/// <returns>The constructed Payload-derived object that was decoded from the binary data.</returns>
public static Payload Decode(BinaryReader reader, DataManager data)
{
var payloadStartPos = reader.BaseStream.Position;
Payload payload = null;
Payload payload;
var initialByte = reader.ReadByte();
reader.BaseStream.Position--;
@ -113,6 +81,39 @@ namespace Dalamud.Game.Text.SeStringHandling
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;
@ -164,18 +165,20 @@ namespace Dalamud.Game.Text.SeStringHandling
break;
case EmbeddedInfoType.LinkTerminator:
// this has no custom handling and so needs to fallthrough to ensure it is captured
// 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:
@ -216,43 +219,126 @@ namespace Dalamud.Game.Text.SeStringHandling
return payload;
}
}
#region parse constants and helpers
/// <summary>
/// Parsing helpers.
/// </summary>
public abstract partial class Payload
{
/// <summary>
/// The start byte of a payload.
/// </summary>
protected const byte START_BYTE = 0x02;
/// <summary>
/// The end byte of a payload.
/// </summary>
protected const byte END_BYTE = 0x03;
protected enum SeStringChunkType
{
Icon = 0x12,
EmphasisItalic = 0x1A,
SeHyphen = 0x1F,
Interactable = 0x27,
AutoTranslateKey = 0x2E,
UIForeground = 0x48,
UIGlow = 0x49
}
/// <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,
DalamudLink = 0x0F, // Dalamud Custom
/// <summary>
/// A custom Dalamud link.
/// </summary>
DalamudLink = 0x0F,
LinkTerminator = 0xCF // not clear but seems to always follow a link
/// <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="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;
if (marker < 0xD0)
return marker - 1;
// the game adds 0xF0 marker for values >= 0xCF
// uasge of 0xD0-0xEF is unknown, should we throw here?
@ -269,6 +355,11 @@ namespace Dalamud.Game.Text.SeStringHandling
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)
@ -287,22 +378,33 @@ namespace Dalamud.Game.Text.SeStringHandling
ret[0] |= (byte)(1 << i);
}
}
ret[0] -= 1;
return ret.ToArray();
}
protected static (uint, uint) GetPackedIntegers(BinaryReader input)
/// <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);
}
protected static byte[] MakePackedInteger(uint val1, uint val2)
/// <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 = (val1 << 16) | (val2 & 0xFFFF);
var value = (high << 16) | (low & 0xFFFF);
return MakeInteger(value);
}
#endregion
}
}

View file

@ -1,4 +1,3 @@
namespace Dalamud.Game.Text.SeStringHandling
{
/// <summary>
@ -10,54 +9,70 @@ namespace Dalamud.Game.Text.SeStringHandling
/// 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 Status Effect link.
/// </summary>
Status,
/// <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 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 an auto-translate dictionary entry.
/// </summary>
AutoTranslateText,
/// <summary>
/// An SeString payload representing italic emphasis formatting on text.
/// </summary>
EmphasisItalic,
/// <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 custom clickable link for dalamud plugins
/// A SeString payload representing a custom clickable link for dalamud plugins.
/// </summary>
DalamudLink,
/// <summary>
/// An SeString payload representing any data we don't handle.
/// </summary>
Unknown,
/// <summary>
/// An SeString payload representing a doublewide SE hypen.
/// </summary>
SeHyphen,
}
}

View file

@ -1,11 +1,12 @@
using Lumina.Excel.GeneratedSheets;
using Serilog;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using Dalamud.Data;
using Lumina.Excel.GeneratedSheets;
using Newtonsoft.Json;
using Serilog;
namespace Dalamud.Game.Text.SeStringHandling.Payloads
{
@ -14,25 +15,7 @@ namespace Dalamud.Game.Text.SeStringHandling.Payloads
/// </summary>
public class AutoTranslatePayload : Payload, ITextProvider
{
public override PayloadType Type => PayloadType.AutoTranslateText;
private string text;
/// <summary>
/// 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
this.text ??= $"{(char)SeIconChar.AutoTranslateOpen} {Resolve()} {(char)SeIconChar.AutoTranslateClose}";
return this.text;
}
}
[JsonProperty]
private uint group;
@ -40,9 +23,8 @@ namespace Dalamud.Game.Text.SeStringHandling.Payloads
[JsonProperty]
private uint key;
internal AutoTranslatePayload() { }
/// <summary>
/// Initializes a new instance of the <see cref="AutoTranslatePayload"/> class.
/// Creates a new auto-translate payload.
/// </summary>
/// <param name="data">DataManager instance needed to resolve game data.</param>
@ -52,19 +34,49 @@ namespace Dalamud.Game.Text.SeStringHandling.Payloads
/// 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(DataManager data, uint group, uint key) {
public AutoTranslatePayload(DataManager data, uint group, uint key)
{
// TODO: friendlier ctor? not sure how to handle that given how weird the tables are
this.DataResolver = data;
this.group = group;
this.key = key;
}
// TODO: friendlier ctor? not sure how to handle that given how weird the tables are
public override string ToString()
/// <summary>
/// Initializes a new instance of the <see cref="AutoTranslatePayload"/> class.
/// </summary>
internal AutoTranslatePayload()
{
return $"{Type} - Group: {group}, Key: {key}, Text: {Text}";
}
/// <inheritdoc/>
public override PayloadType Type => PayloadType.AutoTranslateText;
/// <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);
@ -74,7 +86,7 @@ namespace Dalamud.Game.Text.SeStringHandling.Payloads
{
START_BYTE,
(byte)SeStringChunkType.AutoTranslateKey, (byte)chunkLen,
(byte)this.group
(byte)this.group,
};
bytes.AddRange(keyBytes);
bytes.Add(END_BYTE);
@ -82,6 +94,7 @@ namespace Dalamud.Game.Text.SeStringHandling.Payloads
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
@ -105,7 +118,9 @@ namespace Dalamud.Game.Text.SeStringHandling.Payloads
// (again, if it's meant for another table)
row = sheet.GetRow(this.key);
}
catch { } // don't care, row will be null
catch
{
} // don't care, row will be null
if (row?.Group == this.group)
{
@ -142,7 +157,7 @@ namespace Dalamud.Game.Text.SeStringHandling.Payloads
"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)
_ => throw new Exception(actualTableName),
};
value = name;

View file

@ -1,48 +1,62 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using JetBrains.Annotations;
namespace Dalamud.Game.Text.SeStringHandling.Payloads {
namespace Dalamud.Game.Text.SeStringHandling.Payloads
{
/// <summary>
///
/// This class represents a custom Dalamud clickable chat link.
/// </summary>
public class DalamudLinkPayload : Payload {
public class DalamudLinkPayload : Payload
{
/// <inheritdoc/>
public override PayloadType Type => PayloadType.DalamudLink;
/// <summary>
/// Gets the plugin command ID to be linked.
/// </summary>
public uint CommandId { get; internal set; } = 0;
/// <summary>
/// Gets the plugin name to be linked.
/// </summary>
[NotNull]
public string Plugin { get; internal set; } = string.Empty;
protected override byte[] EncodeImpl() {
var pluginBytes = Encoding.UTF8.GetBytes(Plugin);
var commandBytes = MakeInteger(CommandId);
/// <inheritdoc/>
public override string ToString()
{
return $"{this.Type} - Plugin: {this.Plugin}, Command: {this.CommandId}";
}
/// <inheritdoc/>
protected override byte[] EncodeImpl()
{
var pluginBytes = Encoding.UTF8.GetBytes(this.Plugin);
var commandBytes = MakeInteger(this.CommandId);
var chunkLen = 3 + pluginBytes.Length + commandBytes.Length;
if (chunkLen > 255) {
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);
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();
}
protected override void DecodeImpl(BinaryReader reader, long endOfStream) {
Plugin = Encoding.UTF8.GetString(reader.ReadBytes(reader.ReadByte()));
CommandId = GetInteger(reader);
}
public override string ToString() {
return $"{Type} - Plugin: {Plugin}, Command: {CommandId}";
/// <inheritdoc/>
protected override void DecodeImpl(BinaryReader reader, long endOfStream)
{
this.Plugin = Encoding.UTF8.GetString(reader.ReadBytes(reader.ReadByte()));
this.CommandId = GetInteger(reader);
}
}
}

View file

@ -14,47 +14,58 @@ namespace Dalamud.Game.Text.SeStringHandling.Payloads
public class EmphasisItalicPayload : Payload
{
/// <summary>
/// Payload representing enabling italics on following text.
/// </summary>
public static EmphasisItalicPayload ItalicsOn => new EmphasisItalicPayload(true);
/// <summary>
/// Payload representing disabling italics on following text.
/// </summary>
public static EmphasisItalicPayload ItalicsOff => new EmphasisItalicPayload(false);
public override PayloadType Type => PayloadType.EmphasisItalic;
/// <summary>
/// Whether this payload enables italics formatting for following text.
/// </summary>
public bool IsEnabled { get; private set; }
internal EmphasisItalicPayload() { }
/// <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)
{
IsEnabled = enabled;
this.IsEnabled = enabled;
}
/// <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 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; }
/// <inheritdoc/>
public override PayloadType Type => PayloadType.EmphasisItalic;
/// <inheritdoc/>
public override string ToString()
{
return $"{Type} - Enabled: {IsEnabled}";
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(IsEnabled ? (uint)1 : 0);
var enabledBytes = MakeInteger(this.IsEnabled ? 1u : 0);
var chunkLen = enabledBytes.Length + 1;
var bytes = new List<byte>()
{
START_BYTE, (byte)SeStringChunkType.EmphasisItalic, (byte)chunkLen
START_BYTE, (byte)SeStringChunkType.EmphasisItalic, (byte)chunkLen,
};
bytes.AddRange(enabledBytes);
bytes.Add(END_BYTE);
@ -62,9 +73,10 @@ namespace Dalamud.Game.Text.SeStringHandling.Payloads
return bytes.ToArray();
}
/// <inheritdoc/>
protected override void DecodeImpl(BinaryReader reader, long endOfStream)
{
IsEnabled = (GetInteger(reader) == 1);
this.IsEnabled = GetInteger(reader) == 1;
}
}
}

View file

@ -1,51 +1,71 @@
using System;
using System.Collections.Generic;
using System.IO;
using System;
namespace Dalamud.Game.Text.SeStringHandling.Payloads {
namespace Dalamud.Game.Text.SeStringHandling.Payloads
{
/// <summary>
/// SeString payload representing a bitmap icon from fontIcon
/// SeString payload representing a bitmap icon from fontIcon.
/// </summary>
public class IconPayload : Payload {
public class IconPayload : Payload
{
/// <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)
{
this.Icon = icon;
}
/// <summary>
/// Index of the icon
/// Initializes a new instance of the <see cref="IconPayload"/> class.
/// Create a Icon payload for the specified icon.
/// </summary>
/// <param name="iconIndex">Index of the icon.</param>
[Obsolete("IconPayload(uint) is deprecated, please use IconPayload(BitmapFontIcon).")]
public IconPayload(uint iconIndex)
: this((BitmapFontIcon)iconIndex)
{
}
/// <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 the index of the icon.
/// </summary>
[Obsolete("Use IconPayload.Icon")]
public uint IconIndex => (uint) Icon;
public uint IconIndex => (uint)this.Icon;
/// <summary>
/// Icon the payload represents.
/// Gets or sets the icon the payload represents.
/// </summary>
public BitmapFontIcon Icon { get; set; } = BitmapFontIcon.None;
internal IconPayload() { }
/// <summary>
/// Create a Icon payload for the specified icon.
/// </summary>
/// <param name="iconIndex">Index of the icon</param>
[Obsolete("IconPayload(uint) is deprecated, please use IconPayload(BitmapFontIcon).")]
public IconPayload(uint iconIndex) : this((BitmapFontIcon) iconIndex) { }
/// <summary>
/// Create a Icon payload for the specified icon.
/// </summary>
/// <param name="icon">The Icon</param>
public IconPayload(BitmapFontIcon icon) {
Icon = icon;
/// <inheritdoc />
public override string ToString()
{
return $"{this.Type} - {this.Icon}";
}
/// <inheritdoc />
public override PayloadType Type => PayloadType.Icon;
/// <inheritdoc />
protected override byte[] EncodeImpl() {
var indexBytes = MakeInteger((uint) this.Icon);
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
var bytes = new List<byte>(new byte[]
{
START_BYTE, (byte)SeStringChunkType.Icon, (byte)chunkLen,
});
bytes.AddRange(indexBytes);
bytes.Add(END_BYTE);
@ -53,14 +73,9 @@ namespace Dalamud.Game.Text.SeStringHandling.Payloads {
}
/// <inheritdoc />
protected override void DecodeImpl(BinaryReader reader, long endOfStream) {
Icon = (BitmapFontIcon) GetInteger(reader);
protected override void DecodeImpl(BinaryReader reader, long endOfStream)
{
this.Icon = (BitmapFontIcon)GetInteger(reader);
}
/// <inheritdoc />
public override string ToString() {
return $"{Type} - {Icon}";
}
}
}

View file

@ -1,8 +1,8 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using Dalamud.Data;
using Lumina.Excel.GeneratedSheets;
using Newtonsoft.Json;
@ -14,32 +14,48 @@ namespace Dalamud.Game.Text.SeStringHandling.Payloads
/// </summary>
public class ItemPayload : Payload
{
public override PayloadType Type => PayloadType.Item;
private Item item;
/// <summary>
/// The underlying Lumina Item represented by this payload.
/// </summary>
/// <remarks>
/// Value is evaluated lazily and cached.
/// </remarks>
[JsonIgnore]
public Item Item
{
get
{
this.item ??= this.DataResolver.GetExcelSheet<Item>().GetRow(this.itemId);
return this.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>
/// The displayed name for this item link. Note that incoming links only sometimes have names embedded,
/// 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="data">DataManager instance needed to resolve game data.</param>
/// <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(DataManager data, uint itemId, bool isHQ, string displayNameOverride = null)
{
this.DataResolver = data;
this.itemId = itemId;
this.IsHQ = isHQ;
this.displayName = displayNameOverride;
}
/// <summary>
/// Initializes a new instance of the <see cref="ItemPayload"/> class.
/// Creates a payload representing an interactable item link for the specified item.
/// </summary>
internal ItemPayload()
{
}
/// <inheritdoc/>
public override PayloadType Type => PayloadType.Item;
/// <summary>
/// Gets or sets the displayed name for this item link. Note that incoming links only sometimes have names embedded,
/// often the name is only present in a following text payload.
/// </summary>
public string DisplayName
@ -52,54 +68,44 @@ namespace Dalamud.Game.Text.SeStringHandling.Payloads
set
{
this.displayName = value;
Dirty = true;
this.Dirty = true;
}
}
/// <summary>
/// Whether or not this item link is for a high-quality version of the item.
/// 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;
[JsonProperty]
private uint itemId;
internal ItemPayload() { }
/// <summary>
/// Creates a payload representing an interactable item link for the specified item.
/// </summary>
/// <param name="data">DataManager instance needed to resolve game data.</param>
/// <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(DataManager data, uint itemId, bool isHQ, string displayNameOverride = null) {
this.DataResolver = data;
this.itemId = itemId;
this.IsHQ = isHQ;
this.displayName = displayNameOverride;
}
/// <inheritdoc/>
public override string ToString()
{
return $"{Type} - ItemId: {itemId}, IsHQ: {IsHQ}, Name: {this.displayName ?? Item.Name}";
return $"{this.Type} - ItemId: {this.itemId}, IsHQ: {this.IsHQ}, Name: {this.displayName ?? this.Item.Name}";
}
/// <inheritdoc/>
protected override byte[] EncodeImpl()
{
var actualItemId = IsHQ ? this.itemId + 1000000 : this.itemId;
var actualItemId = this.IsHQ ? this.itemId + 1000000 : this.itemId;
var idBytes = MakeInteger(actualItemId);
bool hasName = !string.IsNullOrEmpty(this.displayName);
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 (IsHQ)
chunkLen += 1 + 1 + this.displayName.Length;
if (this.IsHQ)
{
chunkLen += 4; // unicode representation of the HQ symbol is 3 bytes, preceded by a space
}
@ -108,7 +114,7 @@ namespace Dalamud.Game.Text.SeStringHandling.Payloads
var bytes = new List<byte>()
{
START_BYTE,
(byte)SeStringChunkType.Interactable, (byte)chunkLen, (byte)EmbeddedInfoType.ItemLink
(byte)SeStringChunkType.Interactable, (byte)chunkLen, (byte)EmbeddedInfoType.ItemLink,
};
bytes.AddRange(idBytes);
// unk
@ -118,7 +124,7 @@ namespace Dalamud.Game.Text.SeStringHandling.Payloads
if (hasName)
{
var nameLen = this.displayName.Length + 1;
if (IsHQ)
if (this.IsHQ)
{
nameLen += 4; // space plus 3 bytes for HQ symbol
}
@ -126,11 +132,11 @@ namespace Dalamud.Game.Text.SeStringHandling.Payloads
bytes.AddRange(new byte[]
{
0xFF, // unk
(byte)nameLen
(byte)nameLen,
});
bytes.AddRange(Encoding.UTF8.GetBytes(this.displayName));
if (IsHQ)
if (this.IsHQ)
{
// space and HQ symbol
bytes.AddRange(new byte[] { 0x20, 0xEE, 0x80, 0xBC });
@ -142,6 +148,7 @@ namespace Dalamud.Game.Text.SeStringHandling.Payloads
return bytes.ToArray();
}
/// <inheritdoc/>
protected override void DecodeImpl(BinaryReader reader, long endOfStream)
{
this.itemId = GetInteger(reader);
@ -149,7 +156,7 @@ namespace Dalamud.Game.Text.SeStringHandling.Payloads
if (this.itemId > 1000000)
{
this.itemId -= 1000000;
IsHQ = true;
this.IsHQ = true;
}
if (reader.BaseStream.Position + 3 < endOfStream)
@ -167,7 +174,7 @@ namespace Dalamud.Game.Text.SeStringHandling.Payloads
// 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 (IsHQ)
if (this.IsHQ)
{
itemNameBytes = itemNameBytes.Take(itemNameLen - 4).ToArray();
}

View file

@ -1,8 +1,9 @@
using Lumina.Excel.GeneratedSheets;
using System;
using System.Collections.Generic;
using System.IO;
using Dalamud.Data;
using Lumina.Excel.GeneratedSheets;
using Newtonsoft.Json;
namespace Dalamud.Game.Text.SeStringHandling.Payloads
@ -12,140 +13,19 @@ namespace Dalamud.Game.Text.SeStringHandling.Payloads
/// </summary>
public class MapLinkPayload : Payload
{
public override PayloadType Type => PayloadType.MapLink;
private Map map;
/// <summary>
/// The Map specified for this map link.
/// </summary>
/// <remarks>
/// Value is evaluated lazily and cached.
/// </remarks>
[JsonIgnore]
public Map Map
{
get
{
this.map ??= this.DataResolver.GetExcelSheet<Map>().GetRow(this.mapId);
return this.map;
}
}
private TerritoryType territoryType;
/// <summary>
/// The TerritoryType specified for this map link.
/// </summary>
/// <remarks>
/// Value is evaluated lazily and cached.
/// </remarks>
[JsonIgnore]
public TerritoryType TerritoryType
{
get
{
this.territoryType ??= this.DataResolver.GetExcelSheet<TerritoryType>().GetRow(this.territoryTypeId);
return this.territoryType;
}
}
/// <summary>
/// The internal x-coordinate for this map position.
/// </summary>
public int RawX { get; private set; }
/// <summary>
/// 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>
/// The readable x-coordinate position for this map link. This value is approximate and unrounded.
/// </summary>
public float XCoord
{
get
{
return ConvertRawPositionToMapCoordinate(RawX, Map.SizeFactor);
}
}
/// <summary>
/// The readable y-coordinate position for this map link. This value is approximate and unrounded.
/// </summary>
[JsonIgnore]
public float YCoord
{
get
{
return ConvertRawPositionToMapCoordinate(RawY, Map.SizeFactor);
}
}
/// <summary>
/// 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((XCoord+fudge) * 10.0f) / 10.0f;
var y = Math.Truncate((YCoord+fudge) * 10.0f) / 10.0f;
// the formatting and spacing the game uses
return $"( {x.ToString("0.0")} , {y.ToString("0.0")} )";
}
}
private string placeNameRegion;
/// <summary>
/// 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
{
get
{
this.placeNameRegion ??= TerritoryType.PlaceNameRegion.Value?.Name;
return this.placeNameRegion;
}
}
private string placeName;
/// <summary>
/// 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
{
get
{
this.placeName ??= TerritoryType.PlaceName.Value?.Name;
return this.placeName;
}
}
/// <summary>
/// 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:{TerritoryType.RowId},{Map.RowId},{RawX},{RawY}";
[JsonProperty]
private uint territoryTypeId;
[JsonProperty]
private uint mapId;
// there is no Z; it's purely in the text payload where applicable
internal MapLinkPayload() { }
/// <summary>
/// Initializes a new instance of the <see cref="MapLinkPayload"/> class.
/// Creates an interactable MapLinkPayload from a human-readable position.
/// </summary>
/// <param name="data">DataManager instance needed to resolve game data.</param>
@ -154,18 +34,20 @@ namespace Dalamud.Game.Text.SeStringHandling.Payloads
/// <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(DataManager data, uint territoryTypeId, uint mapId, float niceXCoord, float niceYCoord, float fudgeFactor = 0.05f) {
public MapLinkPayload(DataManager data, uint territoryTypeId, uint mapId, float niceXCoord, float niceYCoord, float fudgeFactor = 0.05f)
{
this.DataResolver = data;
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
RawX = this.ConvertMapCoordinateToRawPosition(niceXCoord + fudgeFactor, Map.SizeFactor);
RawY = this.ConvertMapCoordinateToRawPosition(niceYCoord + fudgeFactor, Map.SizeFactor);
this.RawX = this.ConvertMapCoordinateToRawPosition(niceXCoord + fudgeFactor, this.Map.SizeFactor);
this.RawY = this.ConvertMapCoordinateToRawPosition(niceYCoord + fudgeFactor, this.Map.SizeFactor);
}
/// <summary>
/// Initializes a new instance of the <see cref="MapLinkPayload"/> class.
/// Creates an interactable MapLinkPayload from a raw position.
/// </summary>
/// <param name="data">DataManager instance needed to resolve game data.</param>
@ -178,27 +60,121 @@ namespace Dalamud.Game.Text.SeStringHandling.Payloads
this.DataResolver = data;
this.territoryTypeId = territoryTypeId;
this.mapId = mapId;
RawX = rawX;
RawY = rawY;
this.RawX = rawX;
this.RawY = rawY;
}
/// <summary>
/// Initializes a new instance of the <see cref="MapLinkPayload"/> class.
/// Creates an interactable MapLinkPayload from a human-readable position.
/// </summary>
internal MapLinkPayload()
{
}
/// <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
{
// 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 $"{Type} - TerritoryTypeId: {territoryTypeId}, MapId: {mapId}, RawX: {RawX}, RawY: {RawY}, display: {PlaceName} {CoordinateString}";
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)RawX));
var yBytes = MakeInteger(unchecked((uint)RawY));
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
(byte)SeStringChunkType.Interactable, (byte)chunkLen, (byte)EmbeddedInfoType.MapPositionLink,
};
bytes.AddRange(packedTerritoryAndMapBytes);
@ -211,6 +187,7 @@ namespace Dalamud.Game.Text.SeStringHandling.Payloads
return bytes.ToArray();
}
/// <inheritdoc/>
protected override void DecodeImpl(BinaryReader reader, long endOfStream)
{
// for debugging for now
@ -221,8 +198,8 @@ namespace Dalamud.Game.Text.SeStringHandling.Payloads
try
{
(this.territoryTypeId, this.mapId) = GetPackedIntegers(reader);
RawX = unchecked((int)GetInteger(reader));
RawY = unchecked((int)GetInteger(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
@ -237,6 +214,7 @@ namespace Dalamud.Game.Text.SeStringHandling.Payloads
}
#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)
@ -257,6 +235,7 @@ namespace Dalamud.Game.Text.SeStringHandling.Payloads
return (int)scaledPos;
}
#endregion
}
}

View file

@ -1,9 +1,9 @@
using Lumina.Excel.GeneratedSheets;
using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
using Dalamud.Data;
using Lumina.Excel.GeneratedSheets;
using Newtonsoft.Json;
namespace Dalamud.Game.Text.SeStringHandling.Payloads
@ -13,70 +13,80 @@ namespace Dalamud.Game.Text.SeStringHandling.Payloads
/// </summary>
public class PlayerPayload : Payload
{
public override PayloadType Type => PayloadType.Player;
[JsonProperty]
private string playerName;
/// <summary>
/// The player's displayed name. This does not contain the server name.
/// </summary>
[JsonIgnore]
public string PlayerName
{
get { return this.playerName; }
set
{
this.playerName = value;
Dirty = true;
}
}
private World world;
/// <summary>
/// The Lumina object representing the player's home server.
/// </summary>
/// <remarks>
/// Value is evaluated lazily and cached.
/// </remarks>
[JsonIgnore]
public World World
{
get
{
this.world ??= this.DataResolver.GetExcelSheet<World>().GetRow(this.serverId);
return this.world;
}
}
/// <summary>
/// A 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 => $"{PlayerName}{(char)SeIconChar.CrossWorld}{World.Name}";
[JsonProperty]
private uint serverId;
internal PlayerPayload() { }
[JsonProperty]
private string playerName;
/// <summary>
/// Initializes a new instance of the <see cref="PlayerPayload"/> class.
/// Create a PlayerPayload link for the specified player.
/// </summary>
/// <param name="data">DataManager instance needed to resolve game data.</param>
/// <param name="playerName">The player's displayed name.</param>
/// <param name="serverId">The player's home server id.</param>
public PlayerPayload(DataManager data, string playerName, uint serverId) {
public PlayerPayload(DataManager data, string playerName, uint serverId)
{
this.DataResolver = data;
this.playerName = playerName;
this.serverId = serverId;
}
public override string ToString()
/// <summary>
/// Initializes a new instance of the <see cref="PlayerPayload"/> class.
/// Create a PlayerPayload link for the specified player.
/// </summary>
internal PlayerPayload()
{
return $"{Type} - PlayerName: {PlayerName}, ServerId: {serverId}, ServerName: {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);
/// <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;
}
set
{
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}";
/// <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;
@ -85,9 +95,10 @@ namespace Dalamud.Game.Text.SeStringHandling.Payloads
START_BYTE,
(byte)SeStringChunkType.Interactable, (byte)chunkLen, (byte)EmbeddedInfoType.PlayerName,
/* unk */ 0x01,
(byte)(this.serverId+1), // I didn't want to deal with single-byte values in MakeInteger, so we have to do the +1 manually
/* unk */0x01, /* unk */0xFF, // these sometimes vary but are frequently this
(byte)(this.playerName.Length+1)
(byte)(this.serverId + 1), // I didn't want to deal with single-byte values in MakeInteger, so we have to do the +1 manually
/* unk */ 0x01,
/* unk */ 0xFF, // these sometimes vary but are frequently this
(byte)(this.playerName.Length + 1),
};
bytes.AddRange(Encoding.UTF8.GetBytes(this.playerName));
@ -97,19 +108,20 @@ namespace Dalamud.Game.Text.SeStringHandling.Payloads
// 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(playerName).Encode());
bytes.AddRange(new TextPayload(this.playerName).Encode());
// 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
END_BYTE,
});
return bytes.ToArray();
}
/// <inheritdoc/>
protected override void DecodeImpl(BinaryReader reader, long endOfStream)
{
// unk

View file

@ -1,68 +1,79 @@
using System;
using System.Collections.Generic;
using System.IO;
using Dalamud.Data;
using Lumina.Excel.GeneratedSheets;
using Newtonsoft.Json;
namespace Dalamud.Game.Text.SeStringHandling.Payloads {
namespace Dalamud.Game.Text.SeStringHandling.Payloads
{
/// <summary>
/// An SeString Payload representing an interactable quest link.
/// </summary>
public class QuestPayload : Payload {
public override PayloadType Type => PayloadType.Quest;
public class QuestPayload : Payload
{
private Quest quest;
/// <summary>
/// The underlying Lumina Quest represented by this payload.
/// </summary>
/// <remarks>
/// Value is evaluated lazily and cached.
/// </remarks>
[JsonIgnore]
public Quest Quest {
get {
this.quest ??= this.DataResolver.GetExcelSheet<Quest>().GetRow(this.questId);
return this.quest;
}
}
[JsonProperty]
private uint questId;
internal QuestPayload() { }
/// <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="data">DataManager instance needed to resolve game data.</param>
/// <param name="questId">The id of the quest.</param>
public QuestPayload(DataManager data, uint questId) {
public QuestPayload(DataManager data, uint questId)
{
this.DataResolver = data;
this.questId = questId;
}
/// <inheritdoc />
public override string ToString() {
return $"{Type} - QuestId: {this.questId}, Name: {Quest?.Name ?? "QUEST NOT FOUND"}";
/// <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()
{
}
protected override byte[] EncodeImpl() {
var idBytes = MakeInteger((ushort) this.questId);
/// <inheritdoc/>
public override PayloadType Type => PayloadType.Quest;
/// <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);
/// <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,
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});
bytes.AddRange(new byte[] { 0x01, 0x01, END_BYTE });
return bytes.ToArray();
}
protected override void DecodeImpl(BinaryReader reader, long endOfStream) {
/// <inheritdoc/>
protected override void DecodeImpl(BinaryReader reader, long endOfStream)
{
// Game uses int16, Luimina uses int32
this.questId = GetInteger(reader) + 65536;
}

View file

@ -1,9 +1,10 @@
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using Newtonsoft.Json;
namespace Dalamud.Game.Text.SeStringHandling.Payloads
{
/// <summary>
@ -13,65 +14,91 @@ namespace Dalamud.Game.Text.SeStringHandling.Payloads
/// </summary>
public class RawPayload : Payload
{
// this and others could be an actual static member somewhere and avoid construction costs, but that probably isn't a real concern
/// <summary>
/// A fixed Payload representing a common link-termination sequence, found in many payload chains.
/// </summary>
public static RawPayload LinkTerminator => new RawPayload(new byte[] { 0x02, 0x27, 0x07, 0xCF, 0x01, 0x01, 0x01, 0xFF, 0x01, 0x03 });
public override PayloadType Type => PayloadType.Unknown;
[JsonProperty]
private 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
/// <summary>
/// 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
{
get
{
// for now don't allow modifying the contents
// because we don't really have a way to track Dirty
return (byte[])Encode().Clone();
}
}
[JsonProperty]
private byte chunkType;
[JsonConstructor]
internal RawPayload(byte chunkType)
{
this.chunkType = chunkType;
}
[JsonProperty]
private byte[] data;
/// <summary>
/// Initializes a new instance of the <see cref="RawPayload"/> class.
/// </summary>
/// <param name="data">The payload data.</param>
public RawPayload(byte[] data)
{
// 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();
this.data = data.Skip(3).Take(data.Length - 4).ToArray();
}
public override string ToString()
/// <summary>
/// Initializes a new instance of the <see cref="RawPayload"/> class.
/// </summary>
/// <param name="chunkType">The chunk type.</param>
[JsonConstructor]
internal RawPayload(byte chunkType)
{
return $"{Type} - Data: {BitConverter.ToString(Data).Replace("-", " ")}";
this.chunkType = chunkType;
}
public override bool Equals(object obj) {
if (obj is RawPayload rp) {
if (rp.Data.Length != this.Data.Length) return false;
return !Data.Where((t, i) => rp.Data[i] != t).Any();
/// <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
{
// 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();
}
return false;
}
/// <inheritdoc/>
public override int GetHashCode()
{
// Generated random values.
var hashCode = 1216194372;
hashCode = (hashCode * -1521134295) + this.Type.GetHashCode();
hashCode = (hashCode * -1521134295) + this.chunkType.GetHashCode();
hashCode = (hashCode * -1521134295) + EqualityComparer<byte[]>.Default.GetHashCode(this.data);
return hashCode;
}
/// <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;
@ -80,7 +107,7 @@ namespace Dalamud.Game.Text.SeStringHandling.Payloads
{
START_BYTE,
this.chunkType,
(byte)chunkLen
(byte)chunkLen,
};
bytes.AddRange(this.data);
@ -89,6 +116,7 @@ namespace Dalamud.Game.Text.SeStringHandling.Payloads
return bytes.ToArray();
}
/// <inheritdoc/>
protected override void DecodeImpl(BinaryReader reader, long endOfStream)
{
this.data = reader.ReadBytes((int)(endOfStream - reader.BaseStream.Position + 1));

View file

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

View file

@ -1,8 +1,8 @@
using Lumina.Excel.GeneratedSheets;
using System;
using System.Collections.Generic;
using System.IO;
using Dalamud.Data;
using Lumina.Excel.GeneratedSheets;
using Newtonsoft.Json;
namespace Dalamud.Game.Text.SeStringHandling.Payloads
@ -12,45 +12,50 @@ namespace Dalamud.Game.Text.SeStringHandling.Payloads
/// </summary>
public class StatusPayload : Payload
{
public override PayloadType Type => PayloadType.Status;
private Status status;
/// <summary>
/// The Lumina Status object represented by this payload.
/// </summary>
/// <remarks>
/// Value is evaluated lazily and cached.
/// </remarks>
[JsonIgnore]
public Status Status
{
get
{
status ??= this.DataResolver.GetExcelSheet<Status>().GetRow(this.statusId);
return status;
}
}
[JsonProperty]
private uint statusId;
internal StatusPayload() { }
/// <summary>
/// Initializes a new instance of the <see cref="StatusPayload"/> class.
/// Creates a new StatusPayload for the given status id.
/// </summary>
/// <param name="data">DataManager instance needed to resolve game data.</param>
/// <param name="statusId">The id of the Status for this link.</param>
public StatusPayload(DataManager data, uint statusId) {
public StatusPayload(DataManager data, uint statusId)
{
this.DataResolver = data;
this.statusId = statusId;
}
public override string ToString()
/// <summary>
/// Initializes a new instance of the <see cref="StatusPayload"/> class.
/// Creates a new StatusPayload for the given status id.
/// </summary>
internal StatusPayload()
{
return $"{Type} - StatusId: {statusId}, Name: {Status.Name}";
}
/// <inheritdoc/>
public override PayloadType Type => PayloadType.Status;
/// <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);
/// <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);
@ -58,7 +63,7 @@ namespace Dalamud.Game.Text.SeStringHandling.Payloads
var chunkLen = idBytes.Length + 7;
var bytes = new List<byte>()
{
START_BYTE, (byte)SeStringChunkType.Interactable, (byte)chunkLen, (byte)EmbeddedInfoType.Status
START_BYTE, (byte)SeStringChunkType.Interactable, (byte)chunkLen, (byte)EmbeddedInfoType.Status,
};
bytes.AddRange(idBytes);
@ -68,6 +73,7 @@ namespace Dalamud.Game.Text.SeStringHandling.Payloads
return bytes.ToArray();
}
/// <inheritdoc/>
protected override void DecodeImpl(BinaryReader reader, long endOfStream)
{
this.statusId = GetInteger(reader);

View file

@ -1,9 +1,9 @@
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
using Newtonsoft.Json;
namespace Dalamud.Game.Text.SeStringHandling.Payloads
{
/// <summary>
@ -11,34 +11,11 @@ namespace Dalamud.Game.Text.SeStringHandling.Payloads
/// </summary>
public class TextPayload : Payload, ITextProvider
{
public override PayloadType Type => PayloadType.RawText;
// allow modifying the text of existing payloads on the fly
[JsonProperty]
private string text;
/// <summary>
/// The text contained in this payload.
/// This may contain SE's special unicode characters.
/// </summary>
[JsonIgnore]
public string Text
{
get { return this.text; }
set
{
this.text = value;
Dirty = true;
}
}
public override string ToString()
{
return $"{Type} - Text: {Text}";
}
internal TextPayload() { }
/// <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>
@ -47,6 +24,43 @@ namespace Dalamud.Game.Text.SeStringHandling.Payloads
this.text = 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
{
return this.text;
}
set
{
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
@ -59,6 +73,7 @@ namespace Dalamud.Game.Text.SeStringHandling.Payloads
return Encoding.UTF8.GetBytes(this.text);
}
/// <inheritdoc/>
protected override void DecodeImpl(BinaryReader reader, long endOfStream)
{
var textBytes = new List<byte>();

View file

@ -1,8 +1,8 @@
using Lumina.Excel.GeneratedSheets;
using System;
using System.Collections.Generic;
using System.IO;
using Dalamud.Data;
using Lumina.Excel.GeneratedSheets;
using Newtonsoft.Json;
namespace Dalamud.Game.Text.SeStringHandling.Payloads
@ -12,83 +12,86 @@ namespace Dalamud.Game.Text.SeStringHandling.Payloads
/// </summary>
public class UIForegroundPayload : Payload
{
/// <summary>
/// Payload representing disabling foreground color on following text.
/// </summary>
// TODO Make this work with DI
public static UIForegroundPayload UIForegroundOff => new UIForegroundPayload(null, 0);
public override PayloadType Type => PayloadType.UIForeground;
/// <summary>
/// Whether or not this payload represents applying a foreground color, or disabling one.
/// </summary>
public bool IsEnabled => ColorKey != 0;
private UIColor color;
/// <summary>
/// A Lumina UIColor object representing this payload. The actual color data is at UIColor.UIForeground
/// </summary>
/// <remarks>
/// Value is evaluated lazily and cached.
/// </remarks>
[JsonIgnore]
public UIColor UIColor
{
get
{
this.color ??= this.DataResolver.GetExcelSheet<UIColor>().GetRow(this.colorKey);
return this.color;
}
}
/// <summary>
/// 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;
Dirty = true;
}
}
/// <summary>
/// The Red/Green/Blue values for this foreground color, encoded as a typical hex color.
/// </summary>
[JsonIgnore]
public uint RGB
{
get
{
return (UIColor.UIForeground & 0xFFFFFF);
}
}
[JsonProperty]
private ushort colorKey;
internal UIForegroundPayload() { }
/// <summary>
/// Initializes a new instance of the <see cref="UIForegroundPayload"/> class.
/// Creates a new UIForegroundPayload for the given UIColor key.
/// </summary>
/// <param name="data">DataManager instance needed to resolve game data.</param>
/// <param name="colorKey"></param>
public UIForegroundPayload(DataManager data, ushort colorKey) {
/// <param name="colorKey">A UIColor key.</param>
public UIForegroundPayload(DataManager data, ushort colorKey)
{
this.DataResolver = data;
this.colorKey = colorKey;
}
public override string ToString()
/// <summary>
/// Initializes a new instance of the <see cref="UIForegroundPayload"/> class.
/// Creates a new UIForegroundPayload for the given UIColor key.
/// </summary>
internal UIForegroundPayload()
{
return $"{Type} - UIColor: {colorKey} color: {(IsEnabled ? RGB : 0)}";
}
/// <summary>
/// Gets a payload representing disabling foreground color on following text.
/// </summary>
// TODO Make this work with DI
public static UIForegroundPayload UIForegroundOff => new(null, 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
{
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);
@ -96,7 +99,7 @@ namespace Dalamud.Game.Text.SeStringHandling.Payloads
var bytes = new List<byte>(new byte[]
{
START_BYTE, (byte)SeStringChunkType.UIForeground, (byte)chunkLen
START_BYTE, (byte)SeStringChunkType.UIForeground, (byte)chunkLen,
});
bytes.AddRange(colorBytes);
@ -105,6 +108,7 @@ namespace Dalamud.Game.Text.SeStringHandling.Payloads
return bytes.ToArray();
}
/// <inheritdoc/>
protected override void DecodeImpl(BinaryReader reader, long endOfStream)
{
this.colorKey = (ushort)GetInteger(reader);

View file

@ -1,8 +1,8 @@
using Lumina.Excel.GeneratedSheets;
using System;
using System.Collections.Generic;
using System.IO;
using Dalamud.Data;
using Lumina.Excel.GeneratedSheets;
using Newtonsoft.Json;
namespace Dalamud.Game.Text.SeStringHandling.Payloads
@ -12,83 +12,86 @@ namespace Dalamud.Game.Text.SeStringHandling.Payloads
/// </summary>
public class UIGlowPayload : Payload
{
/// <summary>
/// Payload representing disabling glow color on following text.
/// </summary>
// TODO Make this work with DI
public static UIGlowPayload UIGlowOff => new UIGlowPayload(null, 0);
public override PayloadType Type => PayloadType.UIGlow;
/// <summary>
/// Whether or not this payload represents applying a glow color, or disabling one.
/// </summary>
public bool IsEnabled => ColorKey != 0;
private UIColor color;
/// <summary>
/// A Lumina UIColor object representing this payload. The actual color data is at UIColor.UIGlow
/// </summary>
/// <remarks>
/// Value is evaluated lazily and cached.
/// </remarks>
[JsonIgnore]
public UIColor UIColor
{
get
{
this.color ??= this.DataResolver.GetExcelSheet<UIColor>().GetRow(this.colorKey);
return this.color;
}
}
/// <summary>
/// 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;
Dirty = true;
}
}
/// <summary>
/// The Red/Green/Blue values for this glow color, encoded as a typical hex color.
/// </summary>
[JsonIgnore]
public uint RGB
{
get
{
return (UIColor.UIGlow & 0xFFFFFF);
}
}
[JsonProperty]
private ushort colorKey;
internal UIGlowPayload() { }
/// <summary>
/// Initializes a new instance of the <see cref="UIGlowPayload"/> class.
/// Creates a new UIForegroundPayload for the given UIColor key.
/// </summary>
/// <param name="data">DataManager instance needed to resolve game data.</param>
/// <param name="colorKey"></param>
public UIGlowPayload(DataManager data, ushort colorKey) {
/// <param name="colorKey">A UIColor key.</param>
public UIGlowPayload(DataManager data, ushort colorKey)
{
this.DataResolver = data;
this.colorKey = colorKey;
}
public override string ToString()
/// <summary>
/// Initializes a new instance of the <see cref="UIGlowPayload"/> class.
/// Creates a new UIForegroundPayload for the given UIColor key.
/// </summary>
internal UIGlowPayload()
{
return $"{Type} - UIColor: {colorKey} color: {(IsEnabled ? RGB : 0)}";
}
/// <summary>
/// Gets a payload representing disabling glow color on following text.
/// </summary>
// TODO Make this work with DI
public static UIGlowPayload UIGlowOff => new(null, 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
{
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);
@ -96,7 +99,7 @@ namespace Dalamud.Game.Text.SeStringHandling.Payloads
var bytes = new List<byte>(new byte[]
{
START_BYTE, (byte)SeStringChunkType.UIGlow, (byte)chunkLen
START_BYTE, (byte)SeStringChunkType.UIGlow, (byte)chunkLen,
});
bytes.AddRange(colorBytes);
@ -105,6 +108,7 @@ namespace Dalamud.Game.Text.SeStringHandling.Payloads
return bytes.ToArray();
}
/// <inheritdoc/>
protected override void DecodeImpl(BinaryReader reader, long endOfStream)
{
this.colorKey = (ushort)GetInteger(reader);

View file

@ -1,8 +1,7 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using Dalamud.Data;
using Dalamud.Game.Text.SeStringHandling.Payloads;
using Newtonsoft.Json;
@ -15,21 +14,42 @@ namespace Dalamud.Game.Text.SeStringHandling
public class SeString
{
/// <summary>
/// The ordered list of payloads included in this SeString.
/// 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(Payload[] payloads)
{
this.Payloads = new List<Payload>(payloads);
}
/// <summary>
/// Gets the ordered list of payloads included in this SeString.
/// </summary>
public List<Payload> Payloads { get; }
/// <summary>
/// Helper function to get all raw text from a message as a single joined string
/// 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
/// All the raw text from the contained payloads, joined into a single string.
/// </returns>
public string TextValue
{
get
{
return Payloads
return this.Payloads
.Where(p => p is ITextProvider)
.Cast<ITextProvider>()
.Aggregate(new StringBuilder(), (sb, tp) => sb.Append(tp.Text), sb => sb.ToString());
@ -39,27 +59,45 @@ namespace Dalamud.Game.Text.SeStringHandling
/// <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 SeString(new Payload[] { new TextPayload(str) });
/// <param name="str">string to convert.</param>
/// <returns>Equivalent SeString.</returns>
public static implicit operator SeString(string str) => new(new Payload[] { new TextPayload(str) });
/// <summary>
/// Creates a new SeString from an ordered list of payloads.
/// Creates a SeString from a json. (For testing - not recommended for production use.)
/// </summary>
/// <param name="payloads">The Payload objects to make up this string.</param>
[JsonConstructor]
public SeString(List<Payload> payloads)
/// <param name="json">A serialized SeString produced by ToJson() <see cref="ToJson"/>.</param>
/// <param name="dataManager">An initialized instance of DataManager for Lumina queries.</param>
/// <returns>A SeString initialized with values from the json.</returns>
public static SeString FromJson(string json, DataManager dataManager)
{
Payloads = payloads;
var s = JsonConvert.DeserializeObject<SeString>(json, new JsonSerializerSettings
{
PreserveReferencesHandling = PreserveReferencesHandling.Objects,
TypeNameHandling = TypeNameHandling.Auto,
ConstructorHandling = ConstructorHandling.AllowNonPublicDefaultConstructor,
});
foreach (var payload in s.Payloads)
{
payload.DataResolver = dataManager;
}
return s;
}
/// <summary>
/// Creates a new SeString from an ordered list of payloads.
/// Serializes the SeString to json.
/// </summary>
/// <param name="payloads">The Payload objects to make up this string.</param>
public SeString(Payload[] payloads)
/// <returns>An json representation of this object.</returns>
public string ToJson()
{
Payloads = new List<Payload>(payloads);
return JsonConvert.SerializeObject(this, Formatting.Indented, new JsonSerializerSettings()
{
PreserveReferencesHandling = PreserveReferencesHandling.Objects,
ReferenceLoopHandling = ReferenceLoopHandling.Ignore,
TypeNameHandling = TypeNameHandling.Auto,
});
}
/// <summary>
@ -69,7 +107,7 @@ namespace Dalamud.Game.Text.SeStringHandling
/// <returns>This object.</returns>
public SeString Append(SeString other)
{
Payloads.AddRange(other.Payloads);
this.Payloads.AddRange(other.Payloads);
return this;
}
@ -80,7 +118,7 @@ namespace Dalamud.Game.Text.SeStringHandling
/// <returns>This object.</returns>
public SeString Append(List<Payload> payloads)
{
Payloads.AddRange(payloads);
this.Payloads.AddRange(payloads);
return this;
}
@ -91,7 +129,7 @@ namespace Dalamud.Game.Text.SeStringHandling
/// <returns>This object.</returns>
public SeString Append(Payload payload)
{
Payloads.Add(payload);
this.Payloads.Add(payload);
return this;
}
@ -103,7 +141,7 @@ namespace Dalamud.Game.Text.SeStringHandling
public byte[] Encode()
{
var messageBytes = new List<byte>();
foreach (var p in Payloads)
foreach (var p in this.Payloads)
{
messageBytes.AddRange(p.Encode());
}
@ -114,46 +152,10 @@ namespace Dalamud.Game.Text.SeStringHandling
/// <summary>
/// Get the text value of this SeString.
/// </summary>
/// <returns>The TextValue property</returns>
public override string ToString() {
return TextValue;
}
/// <summary>
/// Serializes the SeString to json
/// </summary>
/// <returns>An json representation of this object</returns>
public string ToJson()
/// <returns>The TextValue property.</returns>
public override string ToString()
{
return JsonConvert.SerializeObject(this, Formatting.Indented, new JsonSerializerSettings()
{
PreserveReferencesHandling = PreserveReferencesHandling.Objects,
ReferenceLoopHandling = ReferenceLoopHandling.Ignore,
TypeNameHandling = TypeNameHandling.Auto
});
}
/// <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>
/// <param name="dataManager">An initialized instance of DataManager for Lumina queries.</param>
/// <returns>A SeString initialized with values from the json</returns>
public static SeString FromJson(string json, DataManager dataManager)
{
var s = JsonConvert.DeserializeObject<SeString>(json, new JsonSerializerSettings
{
PreserveReferencesHandling = PreserveReferencesHandling.Objects,
TypeNameHandling = TypeNameHandling.Auto,
ConstructorHandling = ConstructorHandling.AllowNonPublicDefaultConstructor
});
foreach(var payload in s.Payloads)
{
payload.DataResolver = dataManager;
}
return s;
return this.TextValue;
}
}
}

View file

@ -1,22 +1,27 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Text;
using System.Threading.Tasks;
using Dalamud.Data;
using Dalamud.Game.Text.SeStringHandling.Payloads;
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>
public class SeStringManager
{
private readonly DataManager data;
public SeStringManager(DataManager Data) {
this.data = Data;
/// <summary>
/// Initializes a new instance of the <see cref="SeStringManager"/> class.
/// </summary>
/// <param name="data">The DataManager instance.</param>
public SeStringManager(DataManager data)
{
this.data = data;
}
/// <summary>
@ -51,7 +56,7 @@ namespace Dalamud.Game.Text.SeStringHandling
/// <returns>An SeString containing all the payloads necessary to display an item link in the chat log.</returns>
public SeString CreateItemLink(uint itemId, bool isHQ, string displayNameOverride = null)
{
string displayName = displayNameOverride ?? this.data.GetExcelSheet<Item>().GetRow(itemId).Name;
var displayName = displayNameOverride ?? this.data.GetExcelSheet<Item>().GetRow(itemId).Name;
if (isHQ)
{
displayName += $" {(char)SeIconChar.HighQuality}";
@ -65,11 +70,11 @@ namespace Dalamud.Game.Text.SeStringHandling
new ItemPayload(this.data, itemId, isHQ),
// arrow goes here
new TextPayload(displayName),
RawPayload.LinkTerminator
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());
payloads.InsertRange(3, this.TextArrowPayloads());
return new SeString(payloads);
}
@ -83,9 +88,17 @@ namespace Dalamud.Game.Text.SeStringHandling
/// <returns>An SeString containing all the payloads necessary to display an item link in the chat log.</returns>
public SeString CreateItemLink(Item item, bool isHQ, string displayNameOverride = null)
{
return CreateItemLink((uint)item.RowId, isHQ, displayNameOverride ?? item.Name);
return this.CreateItemLink((uint)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 SeString CreateMapLink(uint territoryId, uint mapId, int rawX, int rawY)
{
var mapPayload = new MapLinkPayload(this.data, territoryId, mapId, rawX, rawY);
@ -96,9 +109,9 @@ namespace Dalamud.Game.Text.SeStringHandling
mapPayload,
// arrow goes here
new TextPayload(nameString),
RawPayload.LinkTerminator
RawPayload.LinkTerminator,
});
payloads.InsertRange(1, TextArrowPayloads());
payloads.InsertRange(1, this.TextArrowPayloads());
return new SeString(payloads);
}
@ -122,9 +135,9 @@ namespace Dalamud.Game.Text.SeStringHandling
mapPayload,
// arrow goes here
new TextPayload(nameString),
RawPayload.LinkTerminator
RawPayload.LinkTerminator,
});
payloads.InsertRange(1, TextArrowPayloads());
payloads.InsertRange(1, this.TextArrowPayloads());
return new SeString(payloads);
}
@ -147,10 +160,10 @@ namespace Dalamud.Game.Text.SeStringHandling
foreach (var place in matches)
{
var map = mapSheet.GetRows().FirstOrDefault(row => row.PlaceName.Row == place.RowId);
var map = mapSheet.FirstOrDefault(row => row.PlaceName.Row == place.RowId);
if (map != null)
{
return CreateMapLink(map.TerritoryType.Row, (uint)map.RowId, xCoord, yCoord, fudgeFactor);
return this.CreateMapLink(map.TerritoryType.Row, map.RowId, xCoord, yCoord, fudgeFactor);
}
}
@ -171,7 +184,7 @@ namespace Dalamud.Game.Text.SeStringHandling
new UIGlowPayload(this.data, 0x01F5),
new TextPayload($"{(char)SeIconChar.LinkMarker}"),
UIGlowPayload.UIGlowOff,
UIForegroundPayload.UIForegroundOff
UIForegroundPayload.UIForegroundOff,
});
}
}