mirror of
https://github.com/goatcorp/Dalamud.git
synced 2026-01-01 05:13:40 +01:00
refactor: Dalamud.Game.Chat => Dalamud.Game.Text, move Sanitizer
This commit is contained in:
parent
7996b64827
commit
3decb57b2c
38 changed files with 141 additions and 140 deletions
120
Dalamud/Game/Text/SeStringHandling/BitmapFontIcon.cs
Normal file
120
Dalamud/Game/Text/SeStringHandling/BitmapFontIcon.cs
Normal file
|
|
@ -0,0 +1,120 @@
|
|||
#pragma warning disable 1591
|
||||
|
||||
namespace Dalamud.Game.Text.SeStringHandling {
|
||||
public enum BitmapFontIcon : uint {
|
||||
None,
|
||||
ControllerDPadUp,
|
||||
ControllerDPadDown,
|
||||
ControllerDPadLeft,
|
||||
ControllerDPadRight,
|
||||
ControllerDPadUpDown,
|
||||
ControllerDPadLeftRight,
|
||||
ControllerDPadAll,
|
||||
|
||||
ControllerButton0, // Xbox B / PS Circle
|
||||
ControllerButton1, // Xbox A / PS Cross
|
||||
ControllerButton2, // Xbox X / PS Square
|
||||
ControllerButton3, // Xbox Y / PS Triangle
|
||||
|
||||
ControllerShoulderLeft,
|
||||
ControllerShoulderRight,
|
||||
|
||||
ControllerTriggerLeft,
|
||||
ControllerTriggerRight,
|
||||
|
||||
ControllerAnalogLeftStickIn,
|
||||
ControllerAnalogRightStickIn,
|
||||
|
||||
ControllerStart,
|
||||
ControllerBack,
|
||||
|
||||
ControllerAnalogLeftStick,
|
||||
ControllerAnalogLeftStickUpDown,
|
||||
ControllerAnalogLeftStickLeftRight,
|
||||
|
||||
ControllerAnalogRightStick,
|
||||
ControllerAnalogRightStickUpDown,
|
||||
ControllerAnalogRightStickLeftRight,
|
||||
|
||||
LaNoscea = 51,
|
||||
BlackShroud,
|
||||
Thanalan,
|
||||
AutoTranslateBegin,
|
||||
AutoTranslateEnd,
|
||||
ElementFire,
|
||||
ElementIce,
|
||||
ElementWind,
|
||||
ElementEarth,
|
||||
ElementLightning,
|
||||
ElementWater,
|
||||
LevelSync,
|
||||
Warning,
|
||||
Ishgard,
|
||||
Aetheryte,
|
||||
Aethernet,
|
||||
|
||||
GoldStar,
|
||||
SilverStar,
|
||||
|
||||
GreenDot = 70,
|
||||
SwordUnsheathed,
|
||||
SwordSheathed,
|
||||
|
||||
Dice,
|
||||
|
||||
FlyZone,
|
||||
FlyZoneLocked,
|
||||
|
||||
NoCircle,
|
||||
|
||||
NewAdventurer,
|
||||
Mentor,
|
||||
MentorPvE,
|
||||
MentorCrafting,
|
||||
MentorPvP,
|
||||
|
||||
Tank,
|
||||
Healer,
|
||||
DPS,
|
||||
Crafter,
|
||||
Gatherer,
|
||||
AnyClass,
|
||||
|
||||
CrossWorld,
|
||||
|
||||
FateSlay,
|
||||
FateBoss,
|
||||
FateGather,
|
||||
FateDefend,
|
||||
FateEscort,
|
||||
FateSpecial1,
|
||||
|
||||
Returner,
|
||||
|
||||
FarEast,
|
||||
GyrAbania,
|
||||
|
||||
FateSpecial2,
|
||||
|
||||
PriorityWorld,
|
||||
|
||||
ElementalLevel,
|
||||
ExclamationRectangle,
|
||||
|
||||
NotoriousMonster,
|
||||
|
||||
Recording,
|
||||
Alarm,
|
||||
|
||||
ArrowUp,
|
||||
ArrowDown,
|
||||
Crystarium,
|
||||
|
||||
MentorProblem,
|
||||
|
||||
FateUnknownGold,
|
||||
|
||||
OrangeDiamond,
|
||||
FateCrafting
|
||||
}
|
||||
}
|
||||
9
Dalamud/Game/Text/SeStringHandling/ITextProvider.cs
Normal file
9
Dalamud/Game/Text/SeStringHandling/ITextProvider.cs
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
using System;
|
||||
|
||||
namespace Dalamud.Game.Text.SeStringHandling
|
||||
{
|
||||
public interface ITextProvider
|
||||
{
|
||||
string Text { get; }
|
||||
}
|
||||
}
|
||||
487
Dalamud/Game/Text/SeStringHandling/Payload.cs
Normal file
487
Dalamud/Game/Text/SeStringHandling/Payload.cs
Normal file
|
|
@ -0,0 +1,487 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using Dalamud.Data;
|
||||
using Dalamud.Game.Text.SeStringHandling.Payloads;
|
||||
using Serilog;
|
||||
|
||||
// TODOs:
|
||||
// - refactor integer handling now that we have multiple packed types
|
||||
// Maybes:
|
||||
// - convert parsing to custom structs for each payload? would make some code prettier and easier to work with
|
||||
// but also wouldn't work out as well for things that are dynamically-sized
|
||||
// - [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 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>
|
||||
public DataManager DataResolver;
|
||||
|
||||
// private for now, since subclasses shouldn't interact with this
|
||||
// To force-invalidate it, Dirty can be set to true
|
||||
private byte[] encodedData;
|
||||
|
||||
/// <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 (Dirty || force)
|
||||
{
|
||||
this.encodedData = EncodeImpl();
|
||||
Dirty = false;
|
||||
}
|
||||
|
||||
return this.encodedData;
|
||||
}
|
||||
|
||||
/// <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, DataManager data)
|
||||
{
|
||||
var payloadStartPos = reader.BaseStream.Position;
|
||||
|
||||
Payload payload = null;
|
||||
|
||||
var initialByte = reader.ReadByte();
|
||||
reader.BaseStream.Position--;
|
||||
if (initialByte != START_BYTE)
|
||||
{
|
||||
payload = DecodeText(reader);
|
||||
}
|
||||
else
|
||||
{
|
||||
payload = DecodeChunk(reader);
|
||||
}
|
||||
|
||||
payload.DataResolver = data;
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
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.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;
|
||||
}
|
||||
|
||||
#region parse constants and helpers
|
||||
|
||||
protected const byte START_BYTE = 0x02;
|
||||
protected const byte END_BYTE = 0x03;
|
||||
|
||||
protected enum SeStringChunkType
|
||||
{
|
||||
Icon = 0x12,
|
||||
EmphasisItalic = 0x1A,
|
||||
SeHyphen = 0x1F,
|
||||
Interactable = 0x27,
|
||||
AutoTranslateKey = 0x2E,
|
||||
UIForeground = 0x48,
|
||||
UIGlow = 0x49
|
||||
}
|
||||
|
||||
public enum EmbeddedInfoType
|
||||
{
|
||||
PlayerName = 0x01,
|
||||
ItemLink = 0x03,
|
||||
MapPositionLink = 0x04,
|
||||
QuestLink = 0x05,
|
||||
Status = 0x09,
|
||||
|
||||
DalamudLink = 0x0F, // Dalamud Custom
|
||||
|
||||
LinkTerminator = 0xCF // not clear but seems to always follow a link
|
||||
}
|
||||
|
||||
|
||||
// TODO - everything below needs to be completely refactored, now that we have run into
|
||||
// a lot more cases than were originally handled.
|
||||
|
||||
protected enum IntegerType
|
||||
{
|
||||
// used as an internal marker; sometimes single bytes are bare with no marker at all
|
||||
None = 0,
|
||||
|
||||
Byte = 0xF0,
|
||||
ByteTimes256 = 0xF1,
|
||||
Int16 = 0xF2,
|
||||
ByteSHL16 = 0xF3,
|
||||
Int16Packed = 0xF4, // seen in map links, seemingly 2 8-bit values packed into 2 bytes with only one marker
|
||||
Int16SHL8 = 0xF5,
|
||||
Int24Special = 0xF6, // unsure how different form Int24 - used for hq items that add 1 million, also used for normal 24-bit values in map links
|
||||
Int8SHL24 = 0xF7,
|
||||
Int8SHL8Int8 = 0xF8,
|
||||
Int8SHL8Int8SHL8 = 0xF9,
|
||||
Int24 = 0xFA,
|
||||
Int16SHL16 = 0xFB,
|
||||
Int24Packed = 0xFC, // used in map links- sometimes short+byte, sometimes... not??
|
||||
Int16Int8SHL8 = 0xFD,
|
||||
Int32 = 0xFE
|
||||
}
|
||||
|
||||
// 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)
|
||||
{
|
||||
var t = input.ReadByte();
|
||||
var type = (IntegerType)t;
|
||||
return GetInteger(input, type);
|
||||
}
|
||||
|
||||
private static uint GetInteger(BinaryReader input, IntegerType type)
|
||||
{
|
||||
const byte ByteLengthCutoff = 0xF0;
|
||||
|
||||
var t = (byte)type;
|
||||
if (t < ByteLengthCutoff)
|
||||
return (uint)(t - 1);
|
||||
|
||||
switch (type)
|
||||
{
|
||||
case IntegerType.Byte:
|
||||
return input.ReadByte();
|
||||
|
||||
case IntegerType.ByteTimes256:
|
||||
return input.ReadByte() * (uint)256;
|
||||
case IntegerType.ByteSHL16:
|
||||
return (uint)(input.ReadByte() << 16);
|
||||
case IntegerType.Int8SHL24:
|
||||
return (uint)(input.ReadByte() << 24);
|
||||
case IntegerType.Int8SHL8Int8:
|
||||
{
|
||||
var v = 0;
|
||||
v |= input.ReadByte() << 24;
|
||||
v |= input.ReadByte();
|
||||
return (uint)v;
|
||||
}
|
||||
case IntegerType.Int8SHL8Int8SHL8:
|
||||
{
|
||||
var v = 0;
|
||||
v |= input.ReadByte() << 24;
|
||||
v |= input.ReadByte() << 8;
|
||||
return (uint)v;
|
||||
}
|
||||
|
||||
|
||||
case IntegerType.Int16:
|
||||
// fallthrough - same logic
|
||||
case IntegerType.Int16Packed:
|
||||
{
|
||||
var v = 0;
|
||||
v |= input.ReadByte() << 8;
|
||||
v |= input.ReadByte();
|
||||
return (uint)v;
|
||||
}
|
||||
case IntegerType.Int16SHL8:
|
||||
{
|
||||
var v = 0;
|
||||
v |= input.ReadByte() << 16;
|
||||
v |= input.ReadByte() << 8;
|
||||
return (uint)v;
|
||||
}
|
||||
case IntegerType.Int16SHL16:
|
||||
{
|
||||
var v = 0;
|
||||
v |= input.ReadByte() << 24;
|
||||
v |= input.ReadByte() << 16;
|
||||
return (uint)v;
|
||||
}
|
||||
|
||||
case IntegerType.Int24Special:
|
||||
// Fallthrough - same logic
|
||||
case IntegerType.Int24Packed:
|
||||
// fallthrough again
|
||||
case IntegerType.Int24:
|
||||
{
|
||||
var v = 0;
|
||||
v |= input.ReadByte() << 16;
|
||||
v |= input.ReadByte() << 8;
|
||||
v |= input.ReadByte();
|
||||
return (uint)v;
|
||||
}
|
||||
case IntegerType.Int16Int8SHL8:
|
||||
{
|
||||
var v = 0;
|
||||
v |= input.ReadByte() << 24;
|
||||
v |= input.ReadByte() << 16;
|
||||
v |= input.ReadByte() << 8;
|
||||
return (uint)v;
|
||||
}
|
||||
case IntegerType.Int32:
|
||||
{
|
||||
var v = 0;
|
||||
v |= input.ReadByte() << 24;
|
||||
v |= input.ReadByte() << 16;
|
||||
v |= input.ReadByte() << 8;
|
||||
v |= input.ReadByte();
|
||||
return (uint)v;
|
||||
}
|
||||
|
||||
default:
|
||||
throw new NotSupportedException();
|
||||
}
|
||||
}
|
||||
|
||||
protected virtual byte[] MakeInteger(uint value, bool withMarker = true, bool incrementSmallInts = true) // TODO: better way to handle this
|
||||
{
|
||||
// single-byte values below the marker values have no marker and have 1 added
|
||||
if (incrementSmallInts && (value + 1 < (int)IntegerType.Byte))
|
||||
{
|
||||
value++;
|
||||
return new byte[] { (byte)value };
|
||||
}
|
||||
|
||||
var bytesPadded = BitConverter.GetBytes(value);
|
||||
Array.Reverse(bytesPadded);
|
||||
var shrunkValue = bytesPadded.SkipWhile(b => b == 0x00).ToArray();
|
||||
|
||||
var encodedNum = new List<byte>();
|
||||
|
||||
if (withMarker)
|
||||
{
|
||||
var marker = GetMarkerForIntegerBytes(shrunkValue);
|
||||
if (marker != 0)
|
||||
{
|
||||
encodedNum.Add(marker);
|
||||
}
|
||||
}
|
||||
|
||||
encodedNum.AddRange(shrunkValue);
|
||||
|
||||
return encodedNum.ToArray();
|
||||
}
|
||||
|
||||
// This is only accurate in a very general sense
|
||||
// Different payloads seem to use different default values for things
|
||||
// So this should be overridden where necessary
|
||||
protected virtual byte GetMarkerForIntegerBytes(byte[] bytes)
|
||||
{
|
||||
// not the most scientific, exists mainly for laziness
|
||||
|
||||
var marker = bytes.Length switch
|
||||
{
|
||||
1 => IntegerType.Byte,
|
||||
2 => IntegerType.Int16,
|
||||
3 => IntegerType.Int24,
|
||||
4 => IntegerType.Int32,
|
||||
_ => throw new NotSupportedException()
|
||||
};
|
||||
|
||||
return (byte)marker;
|
||||
}
|
||||
|
||||
protected virtual byte GetMarkerForPackedIntegerBytes(byte[] bytes)
|
||||
{
|
||||
// unsure if any 'strange' size groupings exist; only ever seen these
|
||||
var type = bytes.Length switch
|
||||
{
|
||||
4 => IntegerType.Int32,
|
||||
3 => IntegerType.Int24Packed,
|
||||
2 => IntegerType.Int16Packed,
|
||||
_ => throw new NotSupportedException()
|
||||
};
|
||||
|
||||
return (byte)type;
|
||||
}
|
||||
|
||||
protected (uint, uint) GetPackedIntegers(BinaryReader input)
|
||||
{
|
||||
// HACK - this was already a hack, but the addition of Int24Packed made it even worse
|
||||
// All of this should be redone/removed at some point
|
||||
|
||||
var marker = (IntegerType)input.ReadByte();
|
||||
input.BaseStream.Position--;
|
||||
|
||||
var value = GetInteger(input);
|
||||
|
||||
if (marker == IntegerType.Int24Packed)
|
||||
{
|
||||
return ((uint)((value & 0xFFFF00) >> 8), (uint)(value & 0xFF));
|
||||
}
|
||||
// this used to be the catchall before Int24Packed; leave it for now to ensure we handle all encodings
|
||||
else // if (marker == IntegerType.Int16Packed || marker == IntegerType.Int32)
|
||||
{
|
||||
if (value > 0xFFFF)
|
||||
{
|
||||
return ((uint)((value & 0xFFFF0000) >> 16), (uint)(value & 0xFFFF));
|
||||
}
|
||||
else if (value > 0xFF)
|
||||
{
|
||||
return ((uint)((value & 0xFF00) >> 8), (uint)(value & 0xFF));
|
||||
}
|
||||
}
|
||||
|
||||
// unsure if there are other cases
|
||||
throw new NotSupportedException();
|
||||
}
|
||||
|
||||
protected byte[] MakePackedInteger(uint val1, uint val2, bool withMarker = true)
|
||||
{
|
||||
var value = MakeInteger(val1, false, false).Concat(MakeInteger(val2, false, false)).ToArray();
|
||||
|
||||
var valueBytes = new List<byte>();
|
||||
if (withMarker)
|
||||
{
|
||||
valueBytes.Add(GetMarkerForPackedIntegerBytes(value));
|
||||
}
|
||||
|
||||
valueBytes.AddRange(value);
|
||||
|
||||
return valueBytes.ToArray();
|
||||
}
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
63
Dalamud/Game/Text/SeStringHandling/PayloadType.cs
Normal file
63
Dalamud/Game/Text/SeStringHandling/PayloadType.cs
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
|
||||
namespace Dalamud.Game.Text.SeStringHandling
|
||||
{
|
||||
/// <summary>
|
||||
/// All parsed types of SeString payloads.
|
||||
/// </summary>
|
||||
public enum PayloadType
|
||||
{
|
||||
/// <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 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 <flag> or <pos>.
|
||||
/// </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
|
||||
/// </summary>
|
||||
DalamudLink,
|
||||
/// <summary>
|
||||
/// An SeString payload representing any data we don't handle.
|
||||
/// </summary>
|
||||
Unknown,
|
||||
SeHyphen,
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,160 @@
|
|||
using Lumina.Excel.GeneratedSheets;
|
||||
using Serilog;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using Dalamud.Data;
|
||||
using Dalamud.Data.TransientSheet;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace Dalamud.Game.Text.SeStringHandling.Payloads
|
||||
{
|
||||
/// <summary>
|
||||
/// An SeString Payload containing an auto-translation/completion chat message.
|
||||
/// </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;
|
||||
|
||||
[JsonProperty]
|
||||
private uint key;
|
||||
|
||||
internal AutoTranslatePayload() { }
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new auto-translate payload.
|
||||
/// </summary>
|
||||
/// <param name="data">DataManager instance needed to resolve game data.</param>
|
||||
/// <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(DataManager data, uint group, uint key) {
|
||||
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()
|
||||
{
|
||||
return $"{Type} - Group: {group}, Key: {key}, Text: {Text}";
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
return bytes.ToArray();
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
catch { } // don't care, row will be null
|
||||
|
||||
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.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}");
|
||||
}
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,48 @@
|
|||
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 {
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
public class DalamudLinkPayload : Payload {
|
||||
public override PayloadType Type => PayloadType.DalamudLink;
|
||||
|
||||
public uint CommandId { get; internal set; } = 0;
|
||||
|
||||
[NotNull]
|
||||
public string Plugin { get; internal set; } = string.Empty;
|
||||
|
||||
protected override byte[] EncodeImpl() {
|
||||
var pluginBytes = Encoding.UTF8.GetBytes(Plugin);
|
||||
var commandBytes = MakeInteger(CommandId);
|
||||
var chunkLen = 3 + pluginBytes.Length + commandBytes.Length;
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
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}";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,70 @@
|
|||
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
|
||||
{
|
||||
/// <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>
|
||||
/// 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;
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return $"{Type} - Enabled: {IsEnabled}";
|
||||
}
|
||||
|
||||
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 chunkLen = enabledBytes.Length + 1;
|
||||
var bytes = new List<byte>()
|
||||
{
|
||||
START_BYTE, (byte)SeStringChunkType.EmphasisItalic, (byte)chunkLen
|
||||
};
|
||||
bytes.AddRange(enabledBytes);
|
||||
bytes.Add(END_BYTE);
|
||||
|
||||
return bytes.ToArray();
|
||||
}
|
||||
|
||||
protected override void DecodeImpl(BinaryReader reader, long endOfStream)
|
||||
{
|
||||
IsEnabled = (GetInteger(reader) == 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
66
Dalamud/Game/Text/SeStringHandling/Payloads/IconPayload.cs
Normal file
66
Dalamud/Game/Text/SeStringHandling/Payloads/IconPayload.cs
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System;
|
||||
|
||||
namespace Dalamud.Game.Text.SeStringHandling.Payloads {
|
||||
|
||||
/// <summary>
|
||||
/// SeString payload representing a bitmap icon from fontIcon
|
||||
/// </summary>
|
||||
public class IconPayload : Payload {
|
||||
|
||||
/// <summary>
|
||||
/// Index of the icon
|
||||
/// </summary>
|
||||
[Obsolete("Use IconPayload.Icon")]
|
||||
public uint IconIndex => (uint) Icon;
|
||||
|
||||
/// <summary>
|
||||
/// 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 PayloadType Type => PayloadType.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) {
|
||||
Icon = (BitmapFontIcon) GetInteger(reader);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override string ToString() {
|
||||
return $"{Type} - {Icon}";
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
214
Dalamud/Game/Text/SeStringHandling/Payloads/ItemPayload.cs
Normal file
214
Dalamud/Game/Text/SeStringHandling/Payloads/ItemPayload.cs
Normal file
|
|
@ -0,0 +1,214 @@
|
|||
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;
|
||||
|
||||
namespace Dalamud.Game.Text.SeStringHandling.Payloads
|
||||
{
|
||||
/// <summary>
|
||||
/// An SeString Payload representing an interactable item link.
|
||||
/// </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;
|
||||
/// <summary>
|
||||
/// The displayed name for this item link. Note that incoming links only sometimes have names embedded,
|
||||
/// often the name is only present in a following text payload.
|
||||
/// </summary>
|
||||
public string DisplayName
|
||||
{
|
||||
get
|
||||
{
|
||||
return this.displayName;
|
||||
}
|
||||
|
||||
set
|
||||
{
|
||||
this.displayName = value;
|
||||
Dirty = true;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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;
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return $"{Type} - ItemId: {itemId}, IsHQ: {IsHQ}, Name: {this.displayName ?? Item.Name}";
|
||||
}
|
||||
|
||||
protected override byte[] EncodeImpl()
|
||||
{
|
||||
var actualItemId = IsHQ ? this.itemId + 1000000 : this.itemId;
|
||||
var idBytes = MakeInteger(actualItemId);
|
||||
bool 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 += 4; // unicode representation of the HQ symbol is 3 bytes, preceded by a space
|
||||
}
|
||||
}
|
||||
|
||||
var bytes = new List<byte>()
|
||||
{
|
||||
START_BYTE,
|
||||
(byte)SeStringChunkType.Interactable, (byte)chunkLen, (byte)EmbeddedInfoType.ItemLink
|
||||
};
|
||||
bytes.AddRange(idBytes);
|
||||
// unk
|
||||
bytes.AddRange(new byte[] { 0x02, 0x01 });
|
||||
|
||||
// Links don't have to include the name, but if they do, it requires additional work
|
||||
if (hasName)
|
||||
{
|
||||
var nameLen = this.displayName.Length + 1;
|
||||
if (IsHQ)
|
||||
{
|
||||
nameLen += 4; // space plus 3 bytes for HQ symbol
|
||||
}
|
||||
|
||||
bytes.AddRange(new byte[]
|
||||
{
|
||||
0xFF, // unk
|
||||
(byte)nameLen
|
||||
});
|
||||
bytes.AddRange(Encoding.UTF8.GetBytes(this.displayName));
|
||||
|
||||
if (IsHQ)
|
||||
{
|
||||
// space and HQ symbol
|
||||
bytes.AddRange(new byte[] { 0x20, 0xEE, 0x80, 0xBC });
|
||||
}
|
||||
}
|
||||
|
||||
bytes.Add(END_BYTE);
|
||||
|
||||
return bytes.ToArray();
|
||||
}
|
||||
|
||||
protected override void DecodeImpl(BinaryReader reader, long endOfStream)
|
||||
{
|
||||
this.itemId = GetInteger(reader);
|
||||
|
||||
if (this.itemId > 1000000)
|
||||
{
|
||||
this.itemId -= 1000000;
|
||||
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 (IsHQ)
|
||||
{
|
||||
itemNameBytes = itemNameBytes.Take(itemNameLen - 4).ToArray();
|
||||
}
|
||||
|
||||
this.displayName = Encoding.UTF8.GetString(itemNameBytes);
|
||||
}
|
||||
}
|
||||
|
||||
protected override byte[] MakeInteger(uint value, bool withMarker = true, bool incrementSmallInts = true)
|
||||
{
|
||||
// TODO: as part of refactor
|
||||
|
||||
// linking an item id that is a multiple of 256 seemingly *requires* using byte*256 marker encoding
|
||||
// or the link will not display correctly
|
||||
// I am unsure if this applies to other data types as well, so keeping localized here, pending the
|
||||
// refactor of all this integer handling mess
|
||||
if (value % 256 == 0)
|
||||
{
|
||||
value /= 256;
|
||||
// this is no longer a small int, but it was likely converted to that range
|
||||
incrementSmallInts = false;
|
||||
}
|
||||
|
||||
return base.MakeInteger(value, withMarker, incrementSmallInts);
|
||||
}
|
||||
|
||||
protected override byte GetMarkerForIntegerBytes(byte[] bytes)
|
||||
{
|
||||
// custom marker just for hq items?
|
||||
if (bytes.Length == 3 && IsHQ)
|
||||
{
|
||||
return (byte)IntegerType.Int24Special;
|
||||
}
|
||||
|
||||
// TODO: as in the above function
|
||||
if (bytes.Length == 1 && (this.itemId % 256 == 0))
|
||||
{
|
||||
return (byte)IntegerType.ByteTimes256;
|
||||
}
|
||||
|
||||
return base.GetMarkerForIntegerBytes(bytes);
|
||||
}
|
||||
}
|
||||
}
|
||||
275
Dalamud/Game/Text/SeStringHandling/Payloads/MapLinkPayload.cs
Normal file
275
Dalamud/Game/Text/SeStringHandling/Payloads/MapLinkPayload.cs
Normal file
|
|
@ -0,0 +1,275 @@
|
|||
using Lumina.Excel.GeneratedSheets;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using Dalamud.Data;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace Dalamud.Game.Text.SeStringHandling.Payloads
|
||||
{
|
||||
/// <summary>
|
||||
/// An SeString Payload representing an interactable map position link.
|
||||
/// </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>
|
||||
/// Creates an interactable MapLinkPayload from a human-readable position.
|
||||
/// </summary>
|
||||
/// <param name="data">DataManager instance needed to resolve game data.</param>
|
||||
/// <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(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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates an interactable MapLinkPayload from a raw position.
|
||||
/// </summary>
|
||||
/// <param name="data">DataManager instance needed to resolve game data.</param>
|
||||
/// <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(DataManager data, uint territoryTypeId, uint mapId, int rawX, int rawY)
|
||||
{
|
||||
this.DataResolver = data;
|
||||
this.territoryTypeId = territoryTypeId;
|
||||
this.mapId = mapId;
|
||||
RawX = rawX;
|
||||
RawY = rawY;
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return $"{Type} - TerritoryTypeId: {territoryTypeId}, MapId: {mapId}, RawX: {RawX}, RawY: {RawY}, display: {PlaceName} {CoordinateString}";
|
||||
}
|
||||
|
||||
protected override byte[] EncodeImpl()
|
||||
{
|
||||
var packedTerritoryAndMapBytes = MakePackedInteger(this.territoryTypeId, this.mapId);
|
||||
var xBytes = MakeInteger(unchecked((uint)RawX));
|
||||
var yBytes = MakeInteger(unchecked((uint)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);
|
||||
|
||||
// unk
|
||||
bytes.AddRange(new byte[] { 0xFF, 0x01, END_BYTE });
|
||||
|
||||
return bytes.ToArray();
|
||||
}
|
||||
|
||||
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);
|
||||
RawX = unchecked((int)GetInteger(reader));
|
||||
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
|
||||
|
||||
protected override byte GetMarkerForIntegerBytes(byte[] bytes)
|
||||
{
|
||||
var type = bytes.Length switch
|
||||
{
|
||||
3 => (byte)IntegerType.Int24Special, // used because seen in incoming data
|
||||
2 => (byte)IntegerType.Int16,
|
||||
1 => (byte)IntegerType.None, // single bytes seem to have no prefix at all here
|
||||
_ => base.GetMarkerForIntegerBytes(bytes)
|
||||
};
|
||||
|
||||
return type;
|
||||
}
|
||||
}
|
||||
}
|
||||
127
Dalamud/Game/Text/SeStringHandling/Payloads/PlayerPayload.cs
Normal file
127
Dalamud/Game/Text/SeStringHandling/Payloads/PlayerPayload.cs
Normal file
|
|
@ -0,0 +1,127 @@
|
|||
using Lumina.Excel.GeneratedSheets;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
using Dalamud.Data;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace Dalamud.Game.Text.SeStringHandling.Payloads
|
||||
{
|
||||
/// <summary>
|
||||
/// An SeString Payload representing a player link.
|
||||
/// </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() { }
|
||||
|
||||
/// <summary>
|
||||
/// 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) {
|
||||
this.DataResolver = data;
|
||||
this.playerName = playerName;
|
||||
this.serverId = serverId;
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return $"{Type} - PlayerName: {PlayerName}, ServerId: {serverId}, ServerName: {World.Name}";
|
||||
}
|
||||
|
||||
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,
|
||||
/* 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)
|
||||
};
|
||||
|
||||
bytes.AddRange(Encoding.UTF8.GetBytes(this.playerName));
|
||||
bytes.Add(END_BYTE);
|
||||
|
||||
// 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(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
|
||||
});
|
||||
|
||||
return bytes.ToArray();
|
||||
}
|
||||
|
||||
protected override void DecodeImpl(BinaryReader reader, long endOfStream)
|
||||
{
|
||||
// unk
|
||||
reader.ReadByte();
|
||||
|
||||
this.serverId = GetInteger(reader);
|
||||
|
||||
// unk
|
||||
reader.ReadBytes(2);
|
||||
|
||||
var nameLen = (int)GetInteger(reader);
|
||||
this.playerName = Encoding.UTF8.GetString(reader.ReadBytes(nameLen));
|
||||
}
|
||||
}
|
||||
}
|
||||
70
Dalamud/Game/Text/SeStringHandling/Payloads/QuestPayload.cs
Normal file
70
Dalamud/Game/Text/SeStringHandling/Payloads/QuestPayload.cs
Normal file
|
|
@ -0,0 +1,70 @@
|
|||
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 {
|
||||
/// <summary>
|
||||
/// An SeString Payload representing an interactable quest link.
|
||||
/// </summary>
|
||||
public class QuestPayload : Payload {
|
||||
public override PayloadType Type => PayloadType.Quest;
|
||||
|
||||
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>
|
||||
/// 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) {
|
||||
this.DataResolver = data;
|
||||
this.questId = questId;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override string ToString() {
|
||||
return $"{Type} - QuestId: {this.questId}, Name: {Quest?.Name ?? "QUEST NOT FOUND"}";
|
||||
}
|
||||
|
||||
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();
|
||||
|
||||
}
|
||||
|
||||
protected override void DecodeImpl(BinaryReader reader, long endOfStream) {
|
||||
// Game uses int16, Luimina uses int32
|
||||
this.questId = GetInteger(reader) + 65536;
|
||||
}
|
||||
}
|
||||
}
|
||||
97
Dalamud/Game/Text/SeStringHandling/Payloads/RawPayload.cs
Normal file
97
Dalamud/Game/Text/SeStringHandling/Payloads/RawPayload.cs
Normal file
|
|
@ -0,0 +1,97 @@
|
|||
using Newtonsoft.Json;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
|
||||
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
|
||||
{
|
||||
// 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;
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return $"{Type} - Data: {BitConverter.ToString(Data).Replace("-", " ")}";
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
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.Add(END_BYTE);
|
||||
|
||||
return bytes.ToArray();
|
||||
}
|
||||
|
||||
protected override void DecodeImpl(BinaryReader reader, long endOfStream)
|
||||
{
|
||||
this.data = reader.ReadBytes((int)(endOfStream - reader.BaseStream.Position + 1));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
using System.IO;
|
||||
|
||||
namespace Dalamud.Game.Text.SeStringHandling.Payloads {
|
||||
/// <summary>
|
||||
/// A wrapped '–'
|
||||
/// </summary>
|
||||
public class SeHyphenPayload : Payload, ITextProvider {
|
||||
|
||||
/// <summary>
|
||||
/// Instance of SeHyphenPayload
|
||||
/// </summary>
|
||||
public static SeHyphenPayload Payload => new SeHyphenPayload();
|
||||
|
||||
/// <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 => "–";
|
||||
}
|
||||
}
|
||||
76
Dalamud/Game/Text/SeStringHandling/Payloads/StatusPayload.cs
Normal file
76
Dalamud/Game/Text/SeStringHandling/Payloads/StatusPayload.cs
Normal file
|
|
@ -0,0 +1,76 @@
|
|||
using Lumina.Excel.GeneratedSheets;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using Dalamud.Data;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace Dalamud.Game.Text.SeStringHandling.Payloads
|
||||
{
|
||||
/// <summary>
|
||||
/// An SeString Payload representing an interactable status link.
|
||||
/// </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>
|
||||
/// 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) {
|
||||
this.DataResolver = data;
|
||||
this.statusId = statusId;
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return $"{Type} - StatusId: {statusId}, Name: {Status.Name}";
|
||||
}
|
||||
|
||||
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 });
|
||||
|
||||
return bytes.ToArray();
|
||||
}
|
||||
|
||||
protected override void DecodeImpl(BinaryReader reader, long endOfStream)
|
||||
{
|
||||
this.statusId = GetInteger(reader);
|
||||
}
|
||||
}
|
||||
}
|
||||
86
Dalamud/Game/Text/SeStringHandling/Payloads/TextPayload.cs
Normal file
86
Dalamud/Game/Text/SeStringHandling/Payloads/TextPayload.cs
Normal file
|
|
@ -0,0 +1,86 @@
|
|||
using Newtonsoft.Json;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
|
||||
namespace Dalamud.Game.Text.SeStringHandling.Payloads
|
||||
{
|
||||
/// <summary>
|
||||
/// An SeString Payload representing a plain text string.
|
||||
/// </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>
|
||||
/// Creates a new TextPayload for the given text.
|
||||
/// </summary>
|
||||
/// <param name="text">The text to include for this payload.</param>
|
||||
public TextPayload(string text)
|
||||
{
|
||||
this.text = text;
|
||||
}
|
||||
|
||||
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 new byte[] { };
|
||||
}
|
||||
|
||||
return Encoding.UTF8.GetBytes(this.text);
|
||||
}
|
||||
|
||||
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());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,123 @@
|
|||
using Lumina.Excel.GeneratedSheets;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using Dalamud.Data;
|
||||
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
|
||||
{
|
||||
/// <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>
|
||||
/// 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) {
|
||||
this.DataResolver = data;
|
||||
this.colorKey = colorKey;
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return $"{Type} - UIColor: {colorKey} color: {(IsEnabled ? RGB : 0)}";
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
return bytes.ToArray();
|
||||
}
|
||||
|
||||
protected override void DecodeImpl(BinaryReader reader, long endOfStream)
|
||||
{
|
||||
this.colorKey = (ushort)GetInteger(reader);
|
||||
}
|
||||
|
||||
protected override byte GetMarkerForIntegerBytes(byte[] bytes)
|
||||
{
|
||||
return bytes.Length switch
|
||||
{
|
||||
// a single byte of 0x01 is used to 'disable' color, and has no marker
|
||||
1 => (byte)IntegerType.None,
|
||||
_ => base.GetMarkerForIntegerBytes(bytes)
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
123
Dalamud/Game/Text/SeStringHandling/Payloads/UIGlowPayload.cs
Normal file
123
Dalamud/Game/Text/SeStringHandling/Payloads/UIGlowPayload.cs
Normal file
|
|
@ -0,0 +1,123 @@
|
|||
using Lumina.Excel.GeneratedSheets;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using Dalamud.Data;
|
||||
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
|
||||
{
|
||||
/// <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>
|
||||
/// 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) {
|
||||
this.DataResolver = data;
|
||||
this.colorKey = colorKey;
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return $"{Type} - UIColor: {colorKey} color: {(IsEnabled ? RGB : 0)}";
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
return bytes.ToArray();
|
||||
}
|
||||
|
||||
protected override void DecodeImpl(BinaryReader reader, long endOfStream)
|
||||
{
|
||||
this.colorKey = (ushort)GetInteger(reader);
|
||||
}
|
||||
|
||||
protected override byte GetMarkerForIntegerBytes(byte[] bytes)
|
||||
{
|
||||
return bytes.Length switch
|
||||
{
|
||||
// a single byte of 0x01 is used to 'disable' color, and has no marker
|
||||
1 => (byte)IntegerType.None,
|
||||
_ => base.GetMarkerForIntegerBytes(bytes)
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
152
Dalamud/Game/Text/SeStringHandling/SeString.cs
Normal file
152
Dalamud/Game/Text/SeStringHandling/SeString.cs
Normal file
|
|
@ -0,0 +1,152 @@
|
|||
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;
|
||||
|
||||
namespace Dalamud.Game.Text.SeStringHandling
|
||||
{
|
||||
/// <summary>
|
||||
/// This class represents a parsed SeString.
|
||||
/// </summary>
|
||||
public class SeString
|
||||
{
|
||||
/// <summary>
|
||||
/// 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
|
||||
/// </summary>
|
||||
/// <returns>
|
||||
/// All the raw text from the contained payloads, joined into a single string
|
||||
/// </returns>
|
||||
public string TextValue
|
||||
{
|
||||
get
|
||||
{
|
||||
return Payloads
|
||||
.Where(p => p is ITextProvider)
|
||||
.Cast<ITextProvider>()
|
||||
.Aggregate(new StringBuilder(), (sb, tp) => sb.Append(tp.Text), sb => sb.ToString());
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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)
|
||||
{
|
||||
Payloads = payloads;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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)
|
||||
{
|
||||
Payloads = new List<Payload>(payloads);
|
||||
}
|
||||
|
||||
/// <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)
|
||||
{
|
||||
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)
|
||||
{
|
||||
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)
|
||||
{
|
||||
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 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 TextValue;
|
||||
}
|
||||
|
||||
/// <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>
|
||||
/// 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
178
Dalamud/Game/Text/SeStringHandling/SeStringManager.cs
Normal file
178
Dalamud/Game/Text/SeStringHandling/SeStringManager.cs
Normal file
|
|
@ -0,0 +1,178 @@
|
|||
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
|
||||
{
|
||||
public class SeStringManager
|
||||
{
|
||||
private readonly DataManager data;
|
||||
|
||||
public SeStringManager(DataManager Data) {
|
||||
this.data = 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>
|
||||
public SeString Parse(byte[] bytes)
|
||||
{
|
||||
var payloads = new List<Payload>();
|
||||
|
||||
using (var stream = new MemoryStream(bytes))
|
||||
using (var reader = new BinaryReader(stream))
|
||||
{
|
||||
while (stream.Position < bytes.Length)
|
||||
{
|
||||
var payload = Payload.Decode(reader, this.data);
|
||||
if (payload != null)
|
||||
payloads.Add(payload);
|
||||
}
|
||||
}
|
||||
|
||||
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="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 SeString CreateItemLink(uint itemId, bool isHQ, string displayNameOverride = null)
|
||||
{
|
||||
string displayName = displayNameOverride ?? this.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(this.data, 0x0225),
|
||||
new UIGlowPayload(this.data, 0x0226),
|
||||
new ItemPayload(this.data, 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());
|
||||
|
||||
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 SeString CreateItemLink(Item item, bool isHQ, string displayNameOverride = null)
|
||||
{
|
||||
return CreateItemLink((uint)item.RowId, isHQ, displayNameOverride ?? item.Name);
|
||||
}
|
||||
|
||||
public SeString CreateMapLink(uint territoryId, uint mapId, int rawX, int rawY)
|
||||
{
|
||||
var mapPayload = new MapLinkPayload(this.data, territoryId, mapId, rawX, rawY);
|
||||
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.
|
||||
/// </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 SeString CreateMapLink(uint territoryId, uint mapId, float xCoord, float yCoord, float fudgeFactor = 0.05f)
|
||||
{
|
||||
var mapPayload = new MapLinkPayload(this.data, 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.
|
||||
/// </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 SeString CreateMapLink(string placeName, float xCoord, float yCoord, float fudgeFactor = 0.05f)
|
||||
{
|
||||
var mapSheet = this.data.GetExcelSheet<Map>();
|
||||
|
||||
var matches = this.data.GetExcelSheet<PlaceName>()
|
||||
.Where(row => row.Name.ToString().ToLowerInvariant() == placeName.ToLowerInvariant())
|
||||
.ToArray();
|
||||
|
||||
foreach (var place in matches)
|
||||
{
|
||||
var map = mapSheet.GetRows().FirstOrDefault(row => row.PlaceName.Row == place.RowId);
|
||||
if (map != null)
|
||||
{
|
||||
return CreateMapLink(map.TerritoryType.Row, (uint)map.RowId, xCoord, yCoord);
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: empty? throw?
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <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>
|
||||
public List<Payload> TextArrowPayloads()
|
||||
{
|
||||
return new List<Payload>(new Payload[]
|
||||
{
|
||||
new UIForegroundPayload(this.data, 0x01F4),
|
||||
new UIGlowPayload(this.data, 0x01F5),
|
||||
new TextPayload($"{(char)SeIconChar.LinkMarker}"),
|
||||
UIGlowPayload.UIGlowOff,
|
||||
UIForegroundPayload.UIForegroundOff
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue