diff --git a/Dalamud/Dalamud.cs b/Dalamud/Dalamud.cs
index 3d39a07ab..105657bc2 100644
--- a/Dalamud/Dalamud.cs
+++ b/Dalamud/Dalamud.cs
@@ -12,6 +12,7 @@ using Dalamud.Data;
using Dalamud.DiscordBot;
using Dalamud.Game;
using Dalamud.Game.Chat;
+using Dalamud.Game.Chat.SeStringHandling;
using Dalamud.Game.ClientState;
using Dalamud.Game.ClientState.Actors.Types;
using Dalamud.Game.ClientState.Actors.Types.NonPlayer;
@@ -116,6 +117,9 @@ namespace Dalamud {
this.Data = new DataManager(this.StartInfo.Language);
await this.Data.Initialize(this.baseDirectory);
+ // TODO: better way to do this? basically for lumina injection
+ SeString.Dalamud = this;
+
this.NetworkHandlers = new NetworkHandlers(this, this.Configuration.OptOutMbCollection);
// Initialize managers. Basically handlers for the logic
@@ -123,7 +127,6 @@ namespace Dalamud {
SetupCommands();
this.ChatHandlers = new ChatHandlers(this);
-
// Discord Bot Manager
this.BotManager = new DiscordBotManager(this, this.Configuration.DiscordFeatureConfig);
this.BotManager.Start();
@@ -558,25 +561,14 @@ namespace Dalamud {
private ItemSearchWindow itemSearchCommandWindow;
private bool isImguiDrawItemSearchWindow;
- private void OnItemLinkCommand(string command, string arguments) {
+ private void OnItemLinkCommand(string command, string arguments)
+ {
this.itemSearchCommandWindow = new ItemSearchWindow(this.Data, new UiBuilder(this.InterfaceManager, "ItemSearcher"), false, arguments);
- this.itemSearchCommandWindow.OnItemChosen += (sender, item) => {
- var hexData = new byte[] {
- 0x02, 0x13, 0x06, 0xFE, 0xFF, 0xF3, 0xF3, 0xF3, 0x03, 0x02, 0x27, 0x07, 0x03, 0xF2, 0x3A, 0x2F,
- 0x02, 0x01, 0x03, 0x02, 0x13, 0x06, 0xFE, 0xFF, 0xFF, 0x7B, 0x1A, 0x03, 0xEE, 0x82, 0xBB, 0x02,
- 0x13, 0x02, 0xEC, 0x03
- };
-
- var endTag = new byte[] {
- 0x02, 0x27, 0x07, 0xCF, 0x01, 0x01, 0x01, 0xFF, 0x01, 0x03, 0x02, 0x13, 0x02, 0xEC, 0x03
- };
-
- BitConverter.GetBytes((short) item.RowId).Reverse().ToArray().CopyTo(hexData, 14);
-
- hexData = hexData.Concat(Encoding.UTF8.GetBytes(item.Name)).Concat(endTag).ToArray();
-
- this.Framework.Gui.Chat.PrintChat(new XivChatEntry {
- MessageBytes = hexData
+ this.itemSearchCommandWindow.OnItemChosen += (sender, item) =>
+ {
+ this.Framework.Gui.Chat.PrintChat(new XivChatEntry
+ {
+ MessageBytes = SeStringUtils.CreateItemLink(item, false).Encode()
});
};
this.isImguiDrawItemSearchWindow = true;
diff --git a/Dalamud/Data/TransientSheet/Completion.cs b/Dalamud/Data/TransientSheet/Completion.cs
new file mode 100644
index 000000000..e437971c3
--- /dev/null
+++ b/Dalamud/Data/TransientSheet/Completion.cs
@@ -0,0 +1,53 @@
+using Lumina.Excel;
+
+namespace Dalamud.Data.TransientSheet
+{
+ [Sheet( "Completion", columnHash: 0x2e6c55a3 )]
+ public class Completion : IExcelRow
+ {
+ // column defs from Mon, 02 Mar 2020 11:00:20 GMT
+
+
+ // col: 03 offset: 0000
+ public string Text;
+
+ // col: 04 offset: 0004
+ public string GroupTitle;
+
+ // col: 02 offset: 0008
+ public string LookupTable;
+
+ // col: 00 offset: 000c
+ public ushort Group;
+
+ // col: 01 offset: 000e
+ public ushort Key;
+
+
+ public int RowId { get; set; }
+ public int SubRowId { get; set; }
+
+ public void PopulateData( RowParser parser, Lumina.Lumina lumina )
+ {
+ RowId = parser.Row;
+ SubRowId = parser.SubRow;
+
+ // col: 3 offset: 0000
+ Text = parser.ReadOffset< string >( 0x0 );
+
+ // col: 4 offset: 0004
+ GroupTitle = parser.ReadOffset< string >( 0x4 );
+
+ // col: 2 offset: 0008
+ LookupTable = parser.ReadOffset< string >( 0x8 );
+
+ // col: 0 offset: 000c
+ Group = parser.ReadOffset< ushort >( 0xc );
+
+ // col: 1 offset: 000e
+ Key = parser.ReadOffset< ushort >( 0xe );
+
+
+ }
+ }
+}
diff --git a/Dalamud/Data/TransientSheet/PetMirage.cs b/Dalamud/Data/TransientSheet/PetMirage.cs
new file mode 100644
index 000000000..42e39337d
--- /dev/null
+++ b/Dalamud/Data/TransientSheet/PetMirage.cs
@@ -0,0 +1,401 @@
+using Lumina.Excel;
+
+namespace Dalamud.Data.TransientSheet
+{
+ [Sheet( "PetMirage", columnHash: 0x720608f1 )]
+ public class PetMirage : IExcelRow
+ {
+ // column defs from Sun, 26 Apr 2020 15:17:06 GMT
+
+
+ // col: 02 offset: 0000
+ public string Name;
+
+ // col: 03 offset: 0004
+ public ushort unknown4;
+
+ // col: 33 offset: 0006
+ public ushort unknown6;
+
+ // col: 48 offset: 0008
+ public ushort unknown8;
+
+ // col: 18 offset: 000a
+ public byte unknowna;
+
+ // col: 04 offset: 000c
+ public ushort unknownc;
+
+ // col: 34 offset: 000e
+ public ushort unknowne;
+
+ // col: 49 offset: 0010
+ public ushort unknown10;
+
+ // col: 19 offset: 0012
+ public byte unknown12;
+
+ // col: 05 offset: 0014
+ public ushort unknown14;
+
+ // col: 35 offset: 0016
+ public ushort unknown16;
+
+ // col: 50 offset: 0018
+ public ushort unknown18;
+
+ // col: 20 offset: 001a
+ public byte unknown1a;
+
+ // col: 06 offset: 001c
+ public ushort unknown1c;
+
+ // col: 36 offset: 001e
+ public ushort unknown1e;
+
+ // col: 51 offset: 0020
+ public ushort unknown20;
+
+ // col: 21 offset: 0022
+ public byte unknown22;
+
+ // col: 07 offset: 0024
+ public ushort unknown24;
+
+ // col: 37 offset: 0026
+ public ushort unknown26;
+
+ // col: 52 offset: 0028
+ public ushort unknown28;
+
+ // col: 22 offset: 002a
+ public byte unknown2a;
+
+ // col: 08 offset: 002c
+ public ushort unknown2c;
+
+ // col: 38 offset: 002e
+ public ushort unknown2e;
+
+ // col: 53 offset: 0030
+ public ushort unknown30;
+
+ // col: 23 offset: 0032
+ public byte unknown32;
+
+ // col: 09 offset: 0034
+ public ushort unknown34;
+
+ // col: 39 offset: 0036
+ public ushort unknown36;
+
+ // col: 54 offset: 0038
+ public ushort unknown38;
+
+ // col: 24 offset: 003a
+ public byte unknown3a;
+
+ // col: 10 offset: 003c
+ public ushort unknown3c;
+
+ // col: 40 offset: 003e
+ public ushort unknown3e;
+
+ // col: 55 offset: 0040
+ public ushort unknown40;
+
+ // col: 25 offset: 0042
+ public byte unknown42;
+
+ // col: 11 offset: 0044
+ public ushort unknown44;
+
+ // col: 41 offset: 0046
+ public ushort unknown46;
+
+ // col: 56 offset: 0048
+ public ushort unknown48;
+
+ // col: 26 offset: 004a
+ public byte unknown4a;
+
+ // col: 12 offset: 004c
+ public ushort unknown4c;
+
+ // col: 42 offset: 004e
+ public ushort unknown4e;
+
+ // col: 57 offset: 0050
+ public ushort unknown50;
+
+ // col: 27 offset: 0052
+ public byte unknown52;
+
+ // col: 13 offset: 0054
+ public ushort unknown54;
+
+ // col: 43 offset: 0056
+ public ushort unknown56;
+
+ // col: 58 offset: 0058
+ public ushort unknown58;
+
+ // col: 28 offset: 005a
+ public byte unknown5a;
+
+ // col: 14 offset: 005c
+ public ushort unknown5c;
+
+ // col: 44 offset: 005e
+ public ushort unknown5e;
+
+ // col: 59 offset: 0060
+ public ushort unknown60;
+
+ // col: 29 offset: 0062
+ public byte unknown62;
+
+ // col: 15 offset: 0064
+ public ushort unknown64;
+
+ // col: 45 offset: 0066
+ public ushort unknown66;
+
+ // col: 60 offset: 0068
+ public ushort unknown68;
+
+ // col: 30 offset: 006a
+ public byte unknown6a;
+
+ // col: 16 offset: 006c
+ public ushort unknown6c;
+
+ // col: 46 offset: 006e
+ public ushort unknown6e;
+
+ // col: 61 offset: 0070
+ public ushort unknown70;
+
+ // col: 31 offset: 0072
+ public byte unknown72;
+
+ // col: 17 offset: 0074
+ public ushort unknown74;
+
+ // col: 47 offset: 0076
+ public ushort unknown76;
+
+ // col: 62 offset: 0078
+ public ushort unknown78;
+
+ // col: 32 offset: 007a
+ public byte unknown7a;
+
+ // col: 00 offset: 007c
+ public float unknown7c;
+
+ // col: 01 offset: 0080
+ public ushort unknown80;
+
+
+ public int RowId { get; set; }
+ public int SubRowId { get; set; }
+
+ public void PopulateData( RowParser parser, Lumina.Lumina lumina )
+ {
+ RowId = parser.Row;
+ SubRowId = parser.SubRow;
+
+ // col: 2 offset: 0000
+ Name = parser.ReadOffset< string >( 0x0 );
+
+ // col: 3 offset: 0004
+ unknown4 = parser.ReadOffset< ushort >( 0x4 );
+
+ // col: 33 offset: 0006
+ unknown6 = parser.ReadOffset< ushort >( 0x6 );
+
+ // col: 48 offset: 0008
+ unknown8 = parser.ReadOffset< ushort >( 0x8 );
+
+ // col: 18 offset: 000a
+ unknowna = parser.ReadOffset< byte >( 0xa );
+
+ // col: 4 offset: 000c
+ unknownc = parser.ReadOffset< ushort >( 0xc );
+
+ // col: 34 offset: 000e
+ unknowne = parser.ReadOffset< ushort >( 0xe );
+
+ // col: 49 offset: 0010
+ unknown10 = parser.ReadOffset< ushort >( 0x10 );
+
+ // col: 19 offset: 0012
+ unknown12 = parser.ReadOffset< byte >( 0x12 );
+
+ // col: 5 offset: 0014
+ unknown14 = parser.ReadOffset< ushort >( 0x14 );
+
+ // col: 35 offset: 0016
+ unknown16 = parser.ReadOffset< ushort >( 0x16 );
+
+ // col: 50 offset: 0018
+ unknown18 = parser.ReadOffset< ushort >( 0x18 );
+
+ // col: 20 offset: 001a
+ unknown1a = parser.ReadOffset< byte >( 0x1a );
+
+ // col: 6 offset: 001c
+ unknown1c = parser.ReadOffset< ushort >( 0x1c );
+
+ // col: 36 offset: 001e
+ unknown1e = parser.ReadOffset< ushort >( 0x1e );
+
+ // col: 51 offset: 0020
+ unknown20 = parser.ReadOffset< ushort >( 0x20 );
+
+ // col: 21 offset: 0022
+ unknown22 = parser.ReadOffset< byte >( 0x22 );
+
+ // col: 7 offset: 0024
+ unknown24 = parser.ReadOffset< ushort >( 0x24 );
+
+ // col: 37 offset: 0026
+ unknown26 = parser.ReadOffset< ushort >( 0x26 );
+
+ // col: 52 offset: 0028
+ unknown28 = parser.ReadOffset< ushort >( 0x28 );
+
+ // col: 22 offset: 002a
+ unknown2a = parser.ReadOffset< byte >( 0x2a );
+
+ // col: 8 offset: 002c
+ unknown2c = parser.ReadOffset< ushort >( 0x2c );
+
+ // col: 38 offset: 002e
+ unknown2e = parser.ReadOffset< ushort >( 0x2e );
+
+ // col: 53 offset: 0030
+ unknown30 = parser.ReadOffset< ushort >( 0x30 );
+
+ // col: 23 offset: 0032
+ unknown32 = parser.ReadOffset< byte >( 0x32 );
+
+ // col: 9 offset: 0034
+ unknown34 = parser.ReadOffset< ushort >( 0x34 );
+
+ // col: 39 offset: 0036
+ unknown36 = parser.ReadOffset< ushort >( 0x36 );
+
+ // col: 54 offset: 0038
+ unknown38 = parser.ReadOffset< ushort >( 0x38 );
+
+ // col: 24 offset: 003a
+ unknown3a = parser.ReadOffset< byte >( 0x3a );
+
+ // col: 10 offset: 003c
+ unknown3c = parser.ReadOffset< ushort >( 0x3c );
+
+ // col: 40 offset: 003e
+ unknown3e = parser.ReadOffset< ushort >( 0x3e );
+
+ // col: 55 offset: 0040
+ unknown40 = parser.ReadOffset< ushort >( 0x40 );
+
+ // col: 25 offset: 0042
+ unknown42 = parser.ReadOffset< byte >( 0x42 );
+
+ // col: 11 offset: 0044
+ unknown44 = parser.ReadOffset< ushort >( 0x44 );
+
+ // col: 41 offset: 0046
+ unknown46 = parser.ReadOffset< ushort >( 0x46 );
+
+ // col: 56 offset: 0048
+ unknown48 = parser.ReadOffset< ushort >( 0x48 );
+
+ // col: 26 offset: 004a
+ unknown4a = parser.ReadOffset< byte >( 0x4a );
+
+ // col: 12 offset: 004c
+ unknown4c = parser.ReadOffset< ushort >( 0x4c );
+
+ // col: 42 offset: 004e
+ unknown4e = parser.ReadOffset< ushort >( 0x4e );
+
+ // col: 57 offset: 0050
+ unknown50 = parser.ReadOffset< ushort >( 0x50 );
+
+ // col: 27 offset: 0052
+ unknown52 = parser.ReadOffset< byte >( 0x52 );
+
+ // col: 13 offset: 0054
+ unknown54 = parser.ReadOffset< ushort >( 0x54 );
+
+ // col: 43 offset: 0056
+ unknown56 = parser.ReadOffset< ushort >( 0x56 );
+
+ // col: 58 offset: 0058
+ unknown58 = parser.ReadOffset< ushort >( 0x58 );
+
+ // col: 28 offset: 005a
+ unknown5a = parser.ReadOffset< byte >( 0x5a );
+
+ // col: 14 offset: 005c
+ unknown5c = parser.ReadOffset< ushort >( 0x5c );
+
+ // col: 44 offset: 005e
+ unknown5e = parser.ReadOffset< ushort >( 0x5e );
+
+ // col: 59 offset: 0060
+ unknown60 = parser.ReadOffset< ushort >( 0x60 );
+
+ // col: 29 offset: 0062
+ unknown62 = parser.ReadOffset< byte >( 0x62 );
+
+ // col: 15 offset: 0064
+ unknown64 = parser.ReadOffset< ushort >( 0x64 );
+
+ // col: 45 offset: 0066
+ unknown66 = parser.ReadOffset< ushort >( 0x66 );
+
+ // col: 60 offset: 0068
+ unknown68 = parser.ReadOffset< ushort >( 0x68 );
+
+ // col: 30 offset: 006a
+ unknown6a = parser.ReadOffset< byte >( 0x6a );
+
+ // col: 16 offset: 006c
+ unknown6c = parser.ReadOffset< ushort >( 0x6c );
+
+ // col: 46 offset: 006e
+ unknown6e = parser.ReadOffset< ushort >( 0x6e );
+
+ // col: 61 offset: 0070
+ unknown70 = parser.ReadOffset< ushort >( 0x70 );
+
+ // col: 31 offset: 0072
+ unknown72 = parser.ReadOffset< byte >( 0x72 );
+
+ // col: 17 offset: 0074
+ unknown74 = parser.ReadOffset< ushort >( 0x74 );
+
+ // col: 47 offset: 0076
+ unknown76 = parser.ReadOffset< ushort >( 0x76 );
+
+ // col: 62 offset: 0078
+ unknown78 = parser.ReadOffset< ushort >( 0x78 );
+
+ // col: 32 offset: 007a
+ unknown7a = parser.ReadOffset< byte >( 0x7a );
+
+ // col: 0 offset: 007c
+ unknown7c = parser.ReadOffset< float >( 0x7c );
+
+ // col: 1 offset: 0080
+ unknown80 = parser.ReadOffset< ushort >( 0x80 );
+
+
+ }
+ }
+}
diff --git a/Dalamud/DiscordBot/DiscordBotManager.cs b/Dalamud/DiscordBot/DiscordBotManager.cs
index 6fec653ed..12c46d9ff 100644
--- a/Dalamud/DiscordBot/DiscordBotManager.cs
+++ b/Dalamud/DiscordBot/DiscordBotManager.cs
@@ -205,10 +205,8 @@ namespace Dalamud.DiscordBot {
senderWorld = this.dalamud.ClientState.LocalPlayer.HomeWorld.GameData.Name;
} else {
- playerLink.Resolve();
-
senderName = wasOutgoingTell ? this.dalamud.ClientState.LocalPlayer.Name : playerLink.PlayerName;
- senderWorld = playerLink.ServerName;
+ senderWorld = playerLink.World.Name;
}
var rawMessage = SeString.Parse(message.RawData).TextValue;
diff --git a/Dalamud/Game/Chat/SeStringHandling/ITextProvider.cs b/Dalamud/Game/Chat/SeStringHandling/ITextProvider.cs
new file mode 100644
index 000000000..9604dbc04
--- /dev/null
+++ b/Dalamud/Game/Chat/SeStringHandling/ITextProvider.cs
@@ -0,0 +1,9 @@
+using System;
+
+namespace Dalamud.Game.Chat.SeStringHandling
+{
+ interface ITextProvider
+ {
+ string Text { get; }
+ }
+}
diff --git a/Dalamud/Game/Chat/SeStringHandling/Payload.cs b/Dalamud/Game/Chat/SeStringHandling/Payload.cs
index 7b31a4565..e809244ec 100644
--- a/Dalamud/Game/Chat/SeStringHandling/Payload.cs
+++ b/Dalamud/Game/Chat/SeStringHandling/Payload.cs
@@ -2,15 +2,17 @@ using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
+using Dalamud.Data;
using Dalamud.Game.Chat.SeStringHandling.Payloads;
using Serilog;
// TODOs:
// - refactor integer handling now that we have multiple packed types
-// - common construction/property design for subclasses
-// - lumina DI
-// - design for handling raw values vs resolved values, both for input and output
-// - wrapper class(es) for handling of composite links in chat (item, map etc) and formatting operations
+// 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.Chat.SeStringHandling
{
@@ -19,33 +21,107 @@ namespace Dalamud.Game.Chat.SeStringHandling
///
public abstract class Payload
{
+ ///
+ /// The type of this payload.
+ ///
public abstract PayloadType Type { get; }
- public abstract void Resolve();
+ ///
+ /// Whether this payload has been modified since the last Encode().
+ ///
+ public bool Dirty { get; protected set; } = true;
- public abstract byte[] Encode();
+ ///
+ /// Encodes the internal state of this payload into a byte[] suitable for sending to in-game
+ /// handlers such as the chat log.
+ ///
+ /// Encoded binary payload data suitable for use with in-game handlers.
+ protected abstract byte[] EncodeImpl();
- protected abstract void ProcessChunkImpl(BinaryReader reader, long endOfStream);
+ // 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
+ ///
+ /// Decodes a byte stream from the game into a payload object.
+ ///
+ /// A BinaryReader containing at least all the data for this payload.
+ /// The location holding the end of the data for this payload.
+ protected abstract void DecodeImpl(BinaryReader reader, long endOfStream);
- public static Payload Process(BinaryReader reader)
+ ///
+ /// The Lumina instance to use for any necessary data lookups.
+ ///
+ protected 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;
+
+ protected Payload()
{
+ // this is not a good way to do this, but I don't want to have to include a dalamud
+ // reference on multiple methods in every payload class
+ // We could also just directly reference this static where we use it, but this at least
+ // allows for more easily changing how this is injected later, without affecting code
+ // that makes use of it
+ this.dataResolver = SeString.Dalamud.Data;
+ }
+
+ ///
+ /// Encode this payload object into a byte[] useable in-game for things like the chat log.
+ ///
+ /// If true, ignores any cached value and forcibly reencodes the payload from its internal representation.
+ /// A byte[] suitable for use with in-game handlers such as the chat log.
+ public byte[] Encode(bool force = false)
+ {
+ if (Dirty || force)
+ {
+ this.encodedData = EncodeImpl();
+ Dirty = false;
+ }
+
+ return this.encodedData;
+ }
+
+ ///
+ /// Decodes a binary representation of a payload into its corresponding nice object payload.
+ ///
+ /// A reader positioned at the start of the payload, and containing at least one entire payload.
+ /// The constructed Payload-derived object that was decoded from the binary data.
+ public static Payload Decode(BinaryReader reader)
+ {
+ var payloadStartPos = reader.BaseStream.Position;
+
Payload payload = null;
var initialByte = reader.ReadByte();
reader.BaseStream.Position--;
if (initialByte != START_BYTE)
{
- payload = ProcessText(reader);
+ payload = DecodeText(reader);
}
else
{
- payload = ProcessChunk(reader);
+ payload = DecodeChunk(reader);
}
+ // for now, cache off the actual binary data for this payload, so we don't have to
+ // regenerate it if the payload isn't modified
+ // TODO: probably better ways to handle this
+ var payloadEndPos = reader.BaseStream.Position;
+
+ reader.BaseStream.Position = payloadStartPos;
+ payload.encodedData = reader.ReadBytes((int)(payloadEndPos - payloadStartPos));
+ payload.Dirty = false;
+
+ // Log.Verbose($"got payload bytes {BitConverter.ToString(payload.encodedData).Replace("-", " ")}");
+
+ reader.BaseStream.Position = payloadEndPos;
+
return payload;
}
- private static Payload ProcessChunk(BinaryReader reader)
+ private static Payload DecodeChunk(BinaryReader reader)
{
Payload payload = null;
@@ -55,8 +131,13 @@ namespace Dalamud.Game.Chat.SeStringHandling
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.Interactable:
{
var subType = (EmbeddedInfoType)reader.ReadByte();
@@ -81,10 +162,13 @@ namespace Dalamud.Game.Chat.SeStringHandling
case EmbeddedInfoType.LinkTerminator:
// this has no custom handling and so needs to fallthrough to ensure it is captured
default:
- Log.Verbose("Unhandled EmbeddedInfoType: {0}", subType);
+ // 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);
- payload = new RawPayload((byte)chunkType);
break;
}
}
@@ -104,11 +188,11 @@ namespace Dalamud.Game.Chat.SeStringHandling
default:
Log.Verbose("Unhandled SeStringChunkType: {0}", chunkType);
- payload = new RawPayload((byte)chunkType);
break;
}
- payload?.ProcessChunkImpl(reader, reader.BaseStream.Position + chunkLen - 1);
+ 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);
@@ -117,10 +201,10 @@ namespace Dalamud.Game.Chat.SeStringHandling
return payload;
}
- private static Payload ProcessText(BinaryReader reader)
+ private static Payload DecodeText(BinaryReader reader)
{
var payload = new TextPayload();
- payload.ProcessChunkImpl(reader, reader.BaseStream.Length);
+ payload.DecodeImpl(reader, reader.BaseStream.Length);
return payload;
}
@@ -132,6 +216,7 @@ namespace Dalamud.Game.Chat.SeStringHandling
protected enum SeStringChunkType
{
+ EmphasisItalic = 0x1A,
Interactable = 0x27,
AutoTranslateKey = 0x2E,
UIForeground = 0x48,
@@ -148,6 +233,10 @@ namespace Dalamud.Game.Chat.SeStringHandling
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
@@ -158,8 +247,8 @@ namespace Dalamud.Game.Chat.SeStringHandling
Int16 = 0xF2,
Int16Packed = 0xF4, // seen in map links, seemingly 2 8-bit values packed into 2 bytes with only one marker
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
- Int24Packed = 0xFC, // used in map links- sometimes short+byte, sometimes... not??
Int24 = 0xFA,
+ Int24Packed = 0xFC, // used in map links- sometimes short+byte, sometimes... not??
Int32 = 0xFE
}
diff --git a/Dalamud/Game/Chat/SeStringHandling/PayloadType.cs b/Dalamud/Game/Chat/SeStringHandling/PayloadType.cs
index 92e8afa44..fe379ffd2 100644
--- a/Dalamud/Game/Chat/SeStringHandling/PayloadType.cs
+++ b/Dalamud/Game/Chat/SeStringHandling/PayloadType.cs
@@ -39,6 +39,10 @@ namespace Dalamud.Game.Chat.SeStringHandling
///
AutoTranslateText,
///
+ /// An SeString payload representing italic emphasis formatting on text.
+ ///
+ EmphasisItalic,
+ ///
/// An SeString payload representing any data we don't handle.
///
Unknown
diff --git a/Dalamud/Game/Chat/SeStringHandling/Payloads/AutoTranslatePayload.cs b/Dalamud/Game/Chat/SeStringHandling/Payloads/AutoTranslatePayload.cs
index 87b25aa45..97e925b11 100644
--- a/Dalamud/Game/Chat/SeStringHandling/Payloads/AutoTranslatePayload.cs
+++ b/Dalamud/Game/Chat/SeStringHandling/Payloads/AutoTranslatePayload.cs
@@ -1,86 +1,75 @@
+using Dalamud.Data.TransientSheet;
+using Lumina.Excel.GeneratedSheets;
+using Serilog;
using System;
using System.Collections.Generic;
using System.IO;
-
+using System.Linq;
namespace Dalamud.Game.Chat.SeStringHandling.Payloads
{
- public class AutoTranslatePayload : Payload
+ ///
+ /// An SeString Payload containing an auto-translation/completion chat message.
+ ///
+ public class AutoTranslatePayload : Payload, ITextProvider
{
public override PayloadType Type => PayloadType.AutoTranslateText;
- public uint Group { get; set; }
-
- public uint Key { get; set; }
-
- public string Text { get; set; }
-
- public override void Resolve()
+ private string text;
+ ///
+ /// The actual text displayed in-game for this payload.
+ ///
+ ///
+ /// Value is evaluated lazily and cached.
+ ///
+ public string Text
{
- // TODO: fixup once lumina DI is in
-
- //if (string.IsNullOrEmpty(Text))
- //{
- // var sheet = dalamud.Data.GetExcelSheet();
-
- // 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(Key);
- // }
- // catch {} // don't care, row will be null
-
- // if (row?.Group == Group)
- // {
- // // if the row exists in this table and the group matches, this is actually the correct data
- // Text = $"{{ {row.Text} }} ";
- // }
- // else
- // {
- // Log.Verbose("row mismatch");
- // 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.GetRows().First(r => r.Group == 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
- // {
- // // TODO: rest of xref'd tables
- // "Action" => dalamud.Data.GetExcelSheet().GetRow(Key).Name,
- // "ClassJob" => dalamud.Data.GetExcelSheet().GetRow(Key).Name,
- // "CraftAction" => dalamud.Data.GetExcelSheet().GetRow(Key).Name,
- // "Mount" => dalamud.Data.GetExcelSheet().GetRow(Key).Singular,
- // "PlaceName" => dalamud.Data.GetExcelSheet().GetRow(Key).Name,
- // "Race" => dalamud.Data.GetExcelSheet().GetRow(Key).Masculine,
- // _ => throw new Exception(actualTableName)
- // };
-
- // Text = $"{{ {name} }} ";
- // }
- // catch (Exception e)
- // {
- // Log.Error(e, $"AutoTranslatePayload - failed to resolve: {this}");
- // }
- // }
- //}
+ 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;
+ }
}
- public override byte[] Encode()
+ private uint group;
+ private uint key;
+
+ internal AutoTranslatePayload() { }
+
+ ///
+ /// Creates a new auto-translate payload.
+ ///
+ /// The group id for this message.
+ /// The key/row id for this message. Which table this is in depends on the group id and details the Completion table.
+ ///
+ /// 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.
+ ///
+ public AutoTranslatePayload(uint group, uint key)
{
- var keyBytes = MakeInteger(Key);
+ 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()
{
START_BYTE,
(byte)SeStringChunkType.AutoTranslateKey, (byte)chunkLen,
- (byte)Group
+ (byte)this.group
};
bytes.AddRange(keyBytes);
bytes.Add(END_BYTE);
@@ -88,18 +77,80 @@ namespace Dalamud.Game.Chat.SeStringHandling.Payloads
return bytes.ToArray();
}
- public override string ToString()
- {
- return $"{Type} - Group: {Group}, Key: {Key}, Text: {Text}";
- }
-
- protected override void ProcessChunkImpl(BinaryReader reader, long endOfStream)
+ 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
- Group = reader.ReadByte();
+ this.group = reader.ReadByte();
- Key = GetInteger(reader);
+ this.key = GetInteger(reader);
+ }
+
+ private string Resolve()
+ {
+ string value = null;
+
+ var sheet = this.dataResolver.GetExcelSheet();
+
+ 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((int)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.GetRows().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 ikey = (int)this.key;
+
+ var name = actualTableName switch
+ {
+ "Action" => this.dataResolver.GetExcelSheet().GetRow(ikey).Name,
+ "ActionComboRoute" => this.dataResolver.GetExcelSheet().GetRow(ikey).Name,
+ "BuddyAction" => this.dataResolver.GetExcelSheet().GetRow(ikey).Name,
+ "ClassJob" => this.dataResolver.GetExcelSheet().GetRow(ikey).Name,
+ "Companion" => this.dataResolver.GetExcelSheet().GetRow(ikey).Singular,
+ "CraftAction" => this.dataResolver.GetExcelSheet().GetRow(ikey).Name,
+ "GeneralAction" => this.dataResolver.GetExcelSheet().GetRow(ikey).Name,
+ "GuardianDeity" => this.dataResolver.GetExcelSheet().GetRow(ikey).Name,
+ "MainCommand" => this.dataResolver.GetExcelSheet().GetRow(ikey).Name,
+ "Mount" => this.dataResolver.GetExcelSheet().GetRow(ikey).Singular,
+ "Pet" => this.dataResolver.GetExcelSheet().GetRow(ikey).Name,
+ "PetAction" => this.dataResolver.GetExcelSheet().GetRow(ikey).Name,
+ "PetMirage" => this.dataResolver.GetExcelSheet().GetRow(ikey).Name,
+ "PlaceName" => this.dataResolver.GetExcelSheet().GetRow(ikey).Name,
+ "Race" => this.dataResolver.GetExcelSheet().GetRow(ikey).Masculine,
+ "TextCommand" => this.dataResolver.GetExcelSheet().GetRow(ikey).Command,
+ "Tribe" => this.dataResolver.GetExcelSheet().GetRow(ikey).Masculine,
+ "Weather" => this.dataResolver.GetExcelSheet().GetRow(ikey).Name,
+ _ => throw new Exception(actualTableName)
+ };
+
+ value = name;
+ }
+ catch (Exception e)
+ {
+ Log.Error(e, $"AutoTranslatePayload - failed to resolve: {this}");
+ }
+ }
+
+ return value;
}
}
}
diff --git a/Dalamud/Game/Chat/SeStringHandling/Payloads/EmphasisItalicPayload.cs b/Dalamud/Game/Chat/SeStringHandling/Payloads/EmphasisItalicPayload.cs
new file mode 100644
index 000000000..341439edb
--- /dev/null
+++ b/Dalamud/Game/Chat/SeStringHandling/Payloads/EmphasisItalicPayload.cs
@@ -0,0 +1,70 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+
+namespace Dalamud.Game.Chat.SeStringHandling.Payloads
+{
+ ///
+ /// An SeString Payload containing information about enabling or disabling italics formatting on following text.
+ ///
+ ///
+ /// As with other formatting payloads, this is only useful in a payload block, where it affects any subsequent
+ /// text payloads.
+ ///
+ public class EmphasisItalicPayload : Payload
+ {
+ ///
+ /// Payload representing enabling italics on following text.
+ ///
+ public static EmphasisItalicPayload ItalicsOn => new EmphasisItalicPayload(true);
+ ///
+ /// Payload representing disabling italics on following text.
+ ///
+ public static EmphasisItalicPayload ItalicsOff => new EmphasisItalicPayload(false);
+
+ public override PayloadType Type => PayloadType.EmphasisItalic;
+
+ ///
+ /// Whether this payload enables italics formatting for following text.
+ ///
+ public bool IsEnabled { get; private set; }
+
+ internal EmphasisItalicPayload() { }
+
+ ///
+ /// Creates an EmphasisItalicPayload.
+ ///
+ /// Whether italics formatting should be enabled or disabled for following text.
+ 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()
+ {
+ 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);
+ }
+ }
+}
diff --git a/Dalamud/Game/Chat/SeStringHandling/Payloads/ItemPayload.cs b/Dalamud/Game/Chat/SeStringHandling/Payloads/ItemPayload.cs
index bc58d488e..87b3ab87d 100644
--- a/Dalamud/Game/Chat/SeStringHandling/Payloads/ItemPayload.cs
+++ b/Dalamud/Game/Chat/SeStringHandling/Payloads/ItemPayload.cs
@@ -1,48 +1,98 @@
+using Dalamud.Data.TransientSheet;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
-using System.Threading.Tasks;
namespace Dalamud.Game.Chat.SeStringHandling.Payloads
{
+ ///
+ /// An SeString Payload representing an interactable item link.
+ ///
public class ItemPayload : Payload
{
public override PayloadType Type => PayloadType.Item;
- public uint ItemId { get; private set; }
- public string ItemName { get; private set; } = string.Empty;
- public bool IsHQ { get; private set; } = false;
-
- public ItemPayload() { }
-
- public ItemPayload(uint itemId, bool isHQ)
+ private Item item;
+ ///
+ /// The underlying Lumina Item represented by this payload.
+ ///
+ ///
+ /// Value is evaluated lazily and cached.
+ ///
+ public Item Item
{
- ItemId = itemId;
- IsHQ = isHQ;
- }
-
- public override void Resolve()
- {
- if (string.IsNullOrEmpty(ItemName))
+ get
{
- dynamic item = XivApi.GetItem((int)ItemId).GetAwaiter().GetResult();
- ItemName = item.Name;
+ this.item ??= this.dataResolver.GetExcelSheet- ().GetRow((int)this.itemId);
+ return this.item;
}
}
- public override byte[] Encode()
+ // 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;
+ ///
+ /// 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.
+ ///
+ public string DisplayName
{
- var actualItemId = IsHQ ? ItemId + 1000000 : ItemId;
+ get
+ {
+ return this.displayName;
+ }
+
+ set
+ {
+ this.displayName = value;
+ Dirty = true;
+ }
+ }
+
+ ///
+ /// Whether or not this item link is for a high-quality version of the item.
+ ///
+ public bool IsHQ { get; private set; } = false;
+
+ private uint itemId;
+
+ internal ItemPayload() { }
+
+ ///
+ /// Creates a payload representing an interactable item link for the specified item.
+ ///
+ /// The id of the item.
+ /// Whether or not the link should be for the high-quality variant of the item.
+ /// 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.
+ public ItemPayload(uint itemId, bool isHQ, string displayNameOverride = null)
+ {
+ 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(ItemName);
+ 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 + ItemName.Length);
+ chunkLen += (1 + 1 + this.displayName.Length);
if (IsHQ)
{
chunkLen += 4; // unicode representation of the HQ symbol is 3 bytes, preceded by a space
@@ -61,7 +111,7 @@ namespace Dalamud.Game.Chat.SeStringHandling.Payloads
// Links don't have to include the name, but if they do, it requires additional work
if (hasName)
{
- var nameLen = ItemName.Length + 1;
+ var nameLen = this.displayName.Length + 1;
if (IsHQ)
{
nameLen += 4; // space plus 3 bytes for HQ symbol
@@ -72,7 +122,7 @@ namespace Dalamud.Game.Chat.SeStringHandling.Payloads
0xFF, // unk
(byte)nameLen
});
- bytes.AddRange(Encoding.UTF8.GetBytes(ItemName));
+ bytes.AddRange(Encoding.UTF8.GetBytes(this.displayName));
if (IsHQ)
{
@@ -86,18 +136,13 @@ namespace Dalamud.Game.Chat.SeStringHandling.Payloads
return bytes.ToArray();
}
- public override string ToString()
+ protected override void DecodeImpl(BinaryReader reader, long endOfStream)
{
- return $"{Type} - ItemId: {ItemId}, ItemName: {ItemName}, IsHQ: {IsHQ}";
- }
+ this.itemId = GetInteger(reader);
- protected override void ProcessChunkImpl(BinaryReader reader, long endOfStream)
- {
- ItemId = GetInteger(reader);
-
- if (ItemId > 1000000)
+ if (this.itemId > 1000000)
{
- ItemId -= 1000000;
+ this.itemId -= 1000000;
IsHQ = true;
}
@@ -109,6 +154,11 @@ namespace Dalamud.Game.Chat.SeStringHandling.Payloads
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)
@@ -116,10 +166,28 @@ namespace Dalamud.Game.Chat.SeStringHandling.Payloads
itemNameBytes = itemNameBytes.Take(itemNameLen - 4).ToArray();
}
- ItemName = Encoding.UTF8.GetString(itemNameBytes);
+ 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?
@@ -128,6 +196,12 @@ namespace Dalamud.Game.Chat.SeStringHandling.Payloads
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);
}
}
diff --git a/Dalamud/Game/Chat/SeStringHandling/Payloads/MapLinkPayload.cs b/Dalamud/Game/Chat/SeStringHandling/Payloads/MapLinkPayload.cs
index f422133d8..e73eb8393 100644
--- a/Dalamud/Game/Chat/SeStringHandling/Payloads/MapLinkPayload.cs
+++ b/Dalamud/Game/Chat/SeStringHandling/Payloads/MapLinkPayload.cs
@@ -5,32 +5,179 @@ using System.IO;
namespace Dalamud.Game.Chat.SeStringHandling.Payloads
{
+ ///
+ /// An SeString Payload representing an interactable map position link.
+ ///
public class MapLinkPayload : Payload
{
public override PayloadType Type => PayloadType.MapLink;
- // pre-Resolve() values
- public uint TerritoryTypeId { get; set; }
- public uint MapId { get; set; }
- public uint RawX { get; set; }
- public uint RawY { get; set; }
+ private Map map;
+ ///
+ /// The Map specified for this map link.
+ ///
+ ///
+ /// Value is evaluated lazily and cached.
+ ///
+ public Map Map
+ {
+ get
+ {
+ this.map ??= this.dataResolver.GetExcelSheet