From 0c4febb6806acfdf68ddad1aa0415451225e7608 Mon Sep 17 00:00:00 2001 From: goat Date: Fri, 7 Feb 2020 02:31:26 +0900 Subject: [PATCH] feat: improve chat string parsing - thanks meli! --- Dalamud/DiscordBot/DiscordBotManager.cs | 53 ++--- Dalamud/Game/Chat/SeString.cs | 157 ------------- Dalamud/Game/Chat/SeStringHandling/Payload.cs | 221 ++++++++++++++++++ .../Game/Chat/SeStringHandling/PayloadType.cs | 31 +++ .../SeStringHandling/Payloads/ItemPayload.cs | 81 +++++++ .../Payloads/PlayerPayload.cs | 85 +++++++ .../Payloads/StatusPayload.cs | 64 +++++ .../SeStringHandling/Payloads/TextPayload.cs | 58 +++++ .../Game/Chat/SeStringHandling/SeString.cs | 107 +++++++++ Dalamud/Game/ChatHandlers.cs | 24 +- 10 files changed, 690 insertions(+), 191 deletions(-) delete mode 100644 Dalamud/Game/Chat/SeString.cs create mode 100644 Dalamud/Game/Chat/SeStringHandling/Payload.cs create mode 100644 Dalamud/Game/Chat/SeStringHandling/PayloadType.cs create mode 100644 Dalamud/Game/Chat/SeStringHandling/Payloads/ItemPayload.cs create mode 100644 Dalamud/Game/Chat/SeStringHandling/Payloads/PlayerPayload.cs create mode 100644 Dalamud/Game/Chat/SeStringHandling/Payloads/StatusPayload.cs create mode 100644 Dalamud/Game/Chat/SeStringHandling/Payloads/TextPayload.cs create mode 100644 Dalamud/Game/Chat/SeStringHandling/SeString.cs diff --git a/Dalamud/DiscordBot/DiscordBotManager.cs b/Dalamud/DiscordBot/DiscordBotManager.cs index d4939d29e..10b56663c 100644 --- a/Dalamud/DiscordBot/DiscordBotManager.cs +++ b/Dalamud/DiscordBot/DiscordBotManager.cs @@ -1,11 +1,15 @@ using System; using System.Collections.Generic; +using System.IO.Ports; using System.Linq; using System.Text; using System.Text.RegularExpressions; using System.Threading; using System.Threading.Tasks; using Dalamud.Game.Chat; +using Dalamud.Game.Chat.SeStringHandling; +using Dalamud.Game.Chat.SeStringHandling.Payloads; +using Dalamud.Game.Internal.Libc; using Discord; using Discord.WebSocket; using Newtonsoft.Json.Linq; @@ -157,12 +161,11 @@ namespace Dalamud.DiscordBot { await channel.SendMessageAsync(embed: embedBuilder.Build()); } - public async Task ProcessChatMessage(XivChatType type, string message, string sender) { + public async Task ProcessChatMessage(XivChatType type, StdString message, StdString sender) { // Special case for outgoing tells, these should be sent under Incoming tells var wasOutgoingTell = false; if (type == XivChatType.TellOutgoing) { type = XivChatType.TellIncoming; - sender = this.dalamud.ClientState.LocalPlayer.Name; wasOutgoingTell = true; } @@ -173,32 +176,34 @@ namespace Dalamud.DiscordBot { return; var chatTypeDetail = type.GetDetails(); - var channels = chatTypeConfigs.Select(c => GetChannel(c.Channel).GetAwaiter().GetResult()); - var senderSplit = sender.Split(new[] {this.worldIcon}, StringSplitOptions.None); + var parsedSender = SeString.Parse(sender.RawData); + var playerLink = parsedSender.Payloads.FirstOrDefault(x => x.Type == PayloadType.Player) as PlayerPayload; - var world = string.Empty; + var senderName = string.Empty; + var senderWorld = string.Empty; - if (this.dalamud.ClientState.Actors.Length > 0) - world = this.dalamud.ClientState.LocalPlayer.CurrentWorld.Name; + if (playerLink == null) { + Log.Error("playerLink was null. Sender: {0}", BitConverter.ToString(sender.RawData)); - if (senderSplit.Length == 2) { - world = senderSplit[1]; - sender = senderSplit[0]; + senderName = wasOutgoingTell ? this.dalamud.ClientState.LocalPlayer.Name : parsedSender.TextValue; + senderWorld = this.dalamud.ClientState.LocalPlayer.HomeWorld.Name; + } else { + playerLink.Resolve(); + + senderName = wasOutgoingTell ? this.dalamud.ClientState.LocalPlayer.Name : playerLink.PlayerName; + senderWorld = playerLink.ServerName; } - sender = SeString.Parse(sender).Output; - message = SeString.Parse(message).Output; + var rawMessage = SeString.Parse(message.RawData).TextValue; - sender = RemoveAllNonLanguageCharacters(sender); - - var avatarUrl = ""; - var lodestoneId = ""; + var avatarUrl = string.Empty; + var lodestoneId = string.Empty; if (!this.config.DisableEmbeds) { - var searchResult = await GetCharacterInfo(sender, world); + var searchResult = await GetCharacterInfo(senderName, senderWorld); lodestoneId = searchResult.LodestoneId; avatarUrl = searchResult.AvatarUrl; @@ -208,9 +213,9 @@ namespace Dalamud.DiscordBot { var name = wasOutgoingTell ? "You" - : sender + (string.IsNullOrEmpty(world) || string.IsNullOrEmpty(sender) + : senderName + (string.IsNullOrEmpty(senderWorld) || string.IsNullOrEmpty(senderName) ? "" - : $" on {world}"); + : $" on {senderWorld}"); for (var chatTypeIndex = 0; chatTypeIndex < chatTypeConfigs.Count(); chatTypeIndex++) { if (!this.config.DisableEmbeds) { @@ -222,7 +227,7 @@ namespace Dalamud.DiscordBot { Name = name, Url = !string.IsNullOrEmpty(lodestoneId) ? "https://eu.finalfantasyxiv.com/lodestone/character/" + lodestoneId : null }, - Description = message, + Description = rawMessage, Timestamp = DateTimeOffset.Now, Footer = new EmbedFooterBuilder { Text = type.GetDetails().FancyName }, Color = new Color((uint)(chatTypeConfigs.ElementAt(chatTypeIndex).Color & 0xFFFFFF)) @@ -253,7 +258,7 @@ namespace Dalamud.DiscordBot { await channels.ElementAt(chatTypeIndex).SendMessageAsync(embed: embedBuilder.Build()); } else { - var simpleMessage = $"{name}: {message}"; + var simpleMessage = $"{name}: {rawMessage}"; if (this.config.CheckForDuplicateMessages) { var recentMsg = this.recentMessages.FirstOrDefault( @@ -267,7 +272,7 @@ namespace Dalamud.DiscordBot { } } - await channels.ElementAt(chatTypeIndex).SendMessageAsync($"**[{chatTypeDetail.Slug}]{name}**: {message}"); + await channels.ElementAt(chatTypeIndex).SendMessageAsync($"**[{chatTypeDetail.Slug}]{name}**: {rawMessage}"); } } } @@ -299,10 +304,6 @@ namespace Dalamud.DiscordBot { return await this.socketClient.GetUser(channelConfig.ChannelId).GetOrCreateDMChannelAsync(); } - private string RemoveAllNonLanguageCharacters(string input) { - return Regex.Replace(input, @"[^\p{L} ']", ""); - } - public void Dispose() { this.socketClient.LogoutAsync().GetAwaiter().GetResult(); } diff --git a/Dalamud/Game/Chat/SeString.cs b/Dalamud/Game/Chat/SeString.cs deleted file mode 100644 index 21203baa0..000000000 --- a/Dalamud/Game/Chat/SeString.cs +++ /dev/null @@ -1,157 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Text; - -namespace Dalamud.Game.Chat { - // TODO: This class does not work - it's a hack, needs a revamp and better handling for payloads used in player chat - public class SeString { - public enum PlayerLinkType { - ItemLink = 0x03 - } - - public enum SeStringPayloadType { - PlayerLink = 0x27 - } - - // in all likelihood these are flags of some kind, but these are the only 2 values I've noticed - public enum ItemQuality { - NormalQuality = 0xF2, - HighQuality = 0xF6 - } - - private const int START_BYTE = 0x02; - private const int END_BYTE = 0x03; - - public static (string Output, List Payloads) Parse(byte[] bytes) - { - var output = new List(); - var payloads = new List(); - - using (var stream = new MemoryStream(bytes)) - using (var reader = new BinaryReader(stream)) - { - while (stream.Position < bytes.Length) - { - var b = stream.ReadByte(); - - if (b == START_BYTE) - ProcessPacket(reader, output, payloads); - else - output.Add((byte)b); - } - } - - return (Encoding.UTF8.GetString(output.ToArray()), payloads); - } - - public static (string Output, List Payloads) Parse(string input) { - var bytes = Encoding.UTF8.GetBytes(input); - return Parse(bytes); - } - - private static void ProcessPacket(BinaryReader reader, List output, - List payloads) { - var type = reader.ReadByte(); - var payloadSize = GetInteger(reader); - - var payload = new byte[payloadSize]; - - reader.Read(payload, 0, payloadSize); - - var orphanByte = reader.Read(); - // If the end of the tag isn't what we predicted, let's ignore it for now - while (orphanByte != END_BYTE) orphanByte = reader.Read(); - - //output.AddRange(Encoding.UTF8.GetBytes($"<{type.ToString("X")}:{BitConverter.ToString(payload)}>")); - - switch ((SeStringPayloadType) type) { - case SeStringPayloadType.PlayerLink: - if (payload[0] == (byte)PlayerLinkType.ItemLink) - { - int itemId; - bool isHQ = payload[1] == (byte)ItemQuality.HighQuality; - if (isHQ) - { - // hq items have an extra 0x0F byte before the ID, and the ID is 0x4240 above the actual item ID - // This _seems_ consistent but I really don't know - itemId = (payload[3] << 8 | payload[4]) - 0x4240; - } - else - { - itemId = (payload[2] << 8 | payload[3]); - } - - payloads.Add(new SeStringPayloadContainer - { - Type = SeStringPayloadType.PlayerLink, - Param1 = (itemId, isHQ) - }); - } - - break; - } - } - - public class SeStringPayloadContainer { - public SeStringPayloadType Type { get; set; } - public object Param1 { get; set; } - } - - #region Shared - - public enum IntegerType { - Byte = 0xF0, - ByteTimes256 = 0xF1, - Int16 = 0xF2, - Int24 = 0xFA, - Int32 = 0xFE - } - - protected static int GetInteger(BinaryReader input) { - var t = input.ReadByte(); - var type = (IntegerType) t; - return GetInteger(input, type); - } - - protected static int GetInteger(BinaryReader input, IntegerType type) { - const byte ByteLengthCutoff = 0xF0; - - var t = (byte) type; - if (t < ByteLengthCutoff) - return t - 1; - - switch (type) { - case IntegerType.Byte: - return input.ReadByte(); - case IntegerType.ByteTimes256: - return input.ReadByte() * 256; - case IntegerType.Int16: { - var v = 0; - v |= input.ReadByte() << 8; - v |= input.ReadByte(); - return v; - } - case IntegerType.Int24: { - var v = 0; - v |= input.ReadByte() << 16; - v |= input.ReadByte() << 8; - v |= input.ReadByte(); - return v; - } - case IntegerType.Int32: { - var v = 0; - v |= input.ReadByte() << 24; - v |= input.ReadByte() << 16; - v |= input.ReadByte() << 8; - v |= input.ReadByte(); - return v; - } - default: - throw new NotSupportedException(); - } - } - - #endregion - } -} diff --git a/Dalamud/Game/Chat/SeStringHandling/Payload.cs b/Dalamud/Game/Chat/SeStringHandling/Payload.cs new file mode 100644 index 000000000..f5ff7aed0 --- /dev/null +++ b/Dalamud/Game/Chat/SeStringHandling/Payload.cs @@ -0,0 +1,221 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Dalamud.Game.Chat.SeStringHandling.Payloads; +using Serilog; + +namespace Dalamud.Game.Chat.SeStringHandling +{ + /// + /// This class represents a parsed SeString payload. + /// + public abstract class Payload + { + public abstract PayloadType Type { get; } + + public abstract void Resolve(); + + public abstract byte[] Encode(); + + protected abstract void ProcessChunkImpl(BinaryReader reader, long endOfStream); + + public static Payload Process(BinaryReader reader) + { + if ((byte)reader.PeekChar() != START_BYTE) + { + return ProcessText(reader); + } + else + { + return ProcessChunk(reader); + } + } + + private static Payload ProcessChunk(BinaryReader reader) + { + Payload payload = null; + + reader.ReadByte(); // START_BYTE + var chunkType = (SeStringChunkType)reader.ReadByte(); + var chunkLen = GetInteger(reader); + + var packetStart = reader.BaseStream.Position; + + switch (chunkType) + { + 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.Status: + payload = new StatusPayload(); + break; + case EmbeddedInfoType.LinkTerminator: + // Does not need to be handled + break; + default: + Log.Verbose("Unhandled EmbeddedInfoType: {0}", subType); + break; + } + } + break; + default: + Log.Verbose("Unhandled SeStringChunkType: {0}", chunkType); + break; + } + + payload?.ProcessChunkImpl(reader, reader.BaseStream.Position + chunkLen - 1); + + // read through the rest of the packet + var readBytes = (int)(reader.BaseStream.Position - packetStart); + reader.ReadBytes(chunkLen - readBytes + 1); // +1 for the END_BYTE marker + + return payload; + } + + private static Payload ProcessText(BinaryReader reader) + { + var payload = new TextPayload(); + payload.ProcessChunkImpl(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 + { + Interactable = 0x27 + } + + protected enum EmbeddedInfoType + { + PlayerName = 0x01, + ItemLink = 0x03, + Status = 0x09, + + LinkTerminator = 0xCF // not clear but seems to always follow a link + } + + protected enum IntegerType + { + Byte = 0xF0, + ByteTimes256 = 0xF1, + Int16 = 0xF2, + Int16Plus1Million = 0xF6, + Int24 = 0xFA, + Int32 = 0xFE + } + + // made protected, unless we actually want to use it externally + // in which case it should probably go live somewhere else + protected static int GetInteger(BinaryReader input) + { + var t = input.ReadByte(); + var type = (IntegerType)t; + return GetInteger(input, type); + } + + private static int GetInteger(BinaryReader input, IntegerType type) + { + const byte ByteLengthCutoff = 0xF0; + + var t = (byte)type; + if (t < ByteLengthCutoff) + return t - 1; + + switch (type) + { + case IntegerType.Byte: + return input.ReadByte(); + case IntegerType.ByteTimes256: + return input.ReadByte() * 256; + case IntegerType.Int16: + { + var v = 0; + v |= input.ReadByte() << 8; + v |= input.ReadByte(); + return v; + } + case IntegerType.Int16Plus1Million: + { + var v = 0; + v |= input.ReadByte() << 16; + v |= input.ReadByte() << 8; + v |= input.ReadByte(); + // need the actual value since it's used as a flag + // v -= 1000000; + return v; + } + case IntegerType.Int24: + { + var v = 0; + v |= input.ReadByte() << 16; + v |= input.ReadByte() << 8; + v |= input.ReadByte(); + return v; + } + case IntegerType.Int32: + { + var v = 0; + v |= input.ReadByte() << 24; + v |= input.ReadByte() << 16; + v |= input.ReadByte() << 8; + v |= input.ReadByte(); + return v; + } + default: + throw new NotSupportedException(); + } + } + + protected static byte[] MakeInteger(int value) + { + // clearly the epitome of efficiency + + var bytesPadded = BitConverter.GetBytes(value); + Array.Reverse(bytesPadded); + return bytesPadded.SkipWhile(b => b == 0x00).ToArray(); + } + + protected static IntegerType GetTypeForIntegerBytes(byte[] bytes) + { + // not the most scientific, exists mainly for laziness + + if (bytes.Length == 1) + { + return IntegerType.Byte; + } + else if (bytes.Length == 2) + { + return IntegerType.Int16; + } + else if (bytes.Length == 3) + { + return IntegerType.Int24; + } + else if (bytes.Length == 4) + { + return IntegerType.Int32; + } + + throw new NotSupportedException(); + } + #endregion + } +} diff --git a/Dalamud/Game/Chat/SeStringHandling/PayloadType.cs b/Dalamud/Game/Chat/SeStringHandling/PayloadType.cs new file mode 100644 index 000000000..87a6a0461 --- /dev/null +++ b/Dalamud/Game/Chat/SeStringHandling/PayloadType.cs @@ -0,0 +1,31 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Dalamud.Game.Chat.SeStringHandling +{ + /// + /// All parsed types of SeString payloads. + /// + public enum PayloadType + { + /// + /// An SeString payload representing a player link. + /// + Player, + /// + /// An SeString payload representing an Item link. + /// + Item, + /// + /// An SeString payload representing an Status Effect link. + /// + Status, + /// + /// An SeString payload representing raw, typed text. + /// + RawText + } +} diff --git a/Dalamud/Game/Chat/SeStringHandling/Payloads/ItemPayload.cs b/Dalamud/Game/Chat/SeStringHandling/Payloads/ItemPayload.cs new file mode 100644 index 000000000..d8bad2c0b --- /dev/null +++ b/Dalamud/Game/Chat/SeStringHandling/Payloads/ItemPayload.cs @@ -0,0 +1,81 @@ +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 +{ + public class ItemPayload : Payload + { + public override PayloadType Type => PayloadType.Item; + + public int ItemId { get; private set; } + public string ItemName { get; private set; } = string.Empty; + public bool IsHQ { get; private set; } = false; + + public ItemPayload() { } + + public ItemPayload(int itemId, bool isHQ) + { + ItemId = itemId; + IsHQ = isHQ; + } + + public override void Resolve() + { + if (string.IsNullOrEmpty(ItemName)) + { + dynamic item = XivApi.GetItem(ItemId).GetAwaiter().GetResult(); + ItemName = item.Name; + } + } + + public override byte[] Encode() + { + var actualItemId = IsHQ ? ItemId + 1000000 : ItemId; + var idBytes = MakeInteger(actualItemId); + + var itemIdFlag = IsHQ ? IntegerType.Int16Plus1Million : IntegerType.Int16; + + var chunkLen = idBytes.Length + 5; + var bytes = new List() + { + START_BYTE, + (byte)SeStringChunkType.Interactable, (byte)chunkLen, (byte)EmbeddedInfoType.ItemLink, + (byte)itemIdFlag + }; + bytes.AddRange(idBytes); + // unk + bytes.AddRange(new byte[] { 0x02, 0x01, END_BYTE }); + + return bytes.ToArray(); + } + + public override string ToString() + { + return $"{Type} - ItemId: {ItemId}, ItemName: {ItemName}, IsHQ: {IsHQ}"; + } + + protected override void ProcessChunkImpl(BinaryReader reader, long endOfStream) + { + ItemId = GetInteger(reader); + + if (ItemId > 1000000) + { + ItemId -= 1000000; + IsHQ = true; + } + + if (reader.BaseStream.Position + 3 < endOfStream) + { + // unk + reader.ReadBytes(3); + + var itemNameLen = GetInteger(reader); + ItemName = Encoding.UTF8.GetString(reader.ReadBytes(itemNameLen)); + } + } + } +} diff --git a/Dalamud/Game/Chat/SeStringHandling/Payloads/PlayerPayload.cs b/Dalamud/Game/Chat/SeStringHandling/Payloads/PlayerPayload.cs new file mode 100644 index 000000000..15f555445 --- /dev/null +++ b/Dalamud/Game/Chat/SeStringHandling/Payloads/PlayerPayload.cs @@ -0,0 +1,85 @@ +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 +{ + public class PlayerPayload : Payload + { + public override PayloadType Type => PayloadType.Player; + + public string PlayerName { get; private set; } + public int ServerId { get; private set; } + public string ServerName { get; private set; } = String.Empty; + + public PlayerPayload() { } + + public PlayerPayload(string playerName, int serverId) + { + PlayerName = playerName; + ServerId = serverId; + } + + public override void Resolve() + { + if (string.IsNullOrEmpty(ServerName)) + { + dynamic server = XivApi.Get($"World/{ServerId}").GetAwaiter().GetResult(); + ServerName = server.Name; + } + } + + public override byte[] Encode() + { + var chunkLen = PlayerName.Length + 7; + var bytes = new List() + { + START_BYTE, + (byte)SeStringChunkType.Interactable, (byte)chunkLen, (byte)EmbeddedInfoType.PlayerName, + /* unk */ 0x01, + (byte)(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)(PlayerName.Length+1) + }; + + bytes.AddRange(Encoding.UTF8.GetBytes(PlayerName)); + bytes.Add(END_BYTE); + + // 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(); + } + + public override string ToString() + { + return $"{Type} - PlayerName: {PlayerName}, ServerId: {ServerId}, ServerName: {ServerName}"; + } + + protected override void ProcessChunkImpl(BinaryReader reader, long endOfStream) + { + // unk + reader.ReadByte(); + + ServerId = GetInteger(reader); + + // unk + reader.ReadBytes(2); + + var nameLen = GetInteger(reader); + PlayerName = Encoding.UTF8.GetString(reader.ReadBytes(nameLen)); + } + } +} diff --git a/Dalamud/Game/Chat/SeStringHandling/Payloads/StatusPayload.cs b/Dalamud/Game/Chat/SeStringHandling/Payloads/StatusPayload.cs new file mode 100644 index 000000000..4169ac42a --- /dev/null +++ b/Dalamud/Game/Chat/SeStringHandling/Payloads/StatusPayload.cs @@ -0,0 +1,64 @@ +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 +{ + public class StatusPayload : Payload + { + public override PayloadType Type => PayloadType.Status; + + public int StatusId { get; private set; } + + public string StatusName { get; private set; } = string.Empty; + + public StatusPayload() { } + + public StatusPayload(int statusId) + { + StatusId = statusId; + } + + public override void Resolve() + { + if (string.IsNullOrEmpty(StatusName)) + { + dynamic status = XivApi.Get($"Status/{StatusId}").GetAwaiter().GetResult(); + //Console.WriteLine($"Resolved status {StatusId} to {status.Name}"); + StatusName = status.Name; + } + } + + public override byte[] Encode() + { + var idBytes = MakeInteger(StatusId); + var idPrefix = GetTypeForIntegerBytes(idBytes); + + var chunkLen = idBytes.Length + 8; + var bytes = new List() + { + START_BYTE, (byte)SeStringChunkType.Interactable, (byte)chunkLen, (byte)EmbeddedInfoType.Status, + (byte)idPrefix + }; + + bytes.AddRange(idBytes); + // unk + bytes.AddRange(new byte[] { 0x01, 0x01, 0xFF, 0x02, 0x20, END_BYTE }); + + return bytes.ToArray(); + } + + public override string ToString() + { + return $"{Type} - StatusId: {StatusId}, StatusName: {StatusName}"; + } + + protected override void ProcessChunkImpl(BinaryReader reader, long endOfStream) + { + StatusId = GetInteger(reader); + } + } +} diff --git a/Dalamud/Game/Chat/SeStringHandling/Payloads/TextPayload.cs b/Dalamud/Game/Chat/SeStringHandling/Payloads/TextPayload.cs new file mode 100644 index 000000000..c2fad1ea9 --- /dev/null +++ b/Dalamud/Game/Chat/SeStringHandling/Payloads/TextPayload.cs @@ -0,0 +1,58 @@ +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 +{ + public class TextPayload : Payload + { + public override PayloadType Type => PayloadType.RawText; + + public string Text { get; private set; } + + public TextPayload() { } + + public TextPayload(string text) + { + Text = text; + } + + public override void Resolve() + { + // nothing to do + } + + public override byte[] Encode() + { + return Encoding.UTF8.GetBytes(Text); + } + + public override string ToString() + { + return $"{Type} - Text: {Text}"; + } + + protected override void ProcessChunkImpl(BinaryReader reader, long endOfStream) + { + var text = new List(); + + while (reader.BaseStream.Position < endOfStream) + { + if ((byte)reader.PeekChar() == START_BYTE) + break; + + // not the most efficient, but the easiest + text.Add(reader.ReadByte()); + } + + if (text.Count > 0) + { + // TODO: handling of the game's assorted special unicode characters + Text = Encoding.UTF8.GetString(text.ToArray()); + } + } + } +} diff --git a/Dalamud/Game/Chat/SeStringHandling/SeString.cs b/Dalamud/Game/Chat/SeStringHandling/SeString.cs new file mode 100644 index 000000000..f0ab270b7 --- /dev/null +++ b/Dalamud/Game/Chat/SeStringHandling/SeString.cs @@ -0,0 +1,107 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Dalamud.Game.Chat.SeStringHandling.Payloads; + +namespace Dalamud.Game.Chat.SeStringHandling +{ + /// + /// This class represents a parsed SeString. + /// + public class SeString + { + private Dictionary> mappedPayloads_ = null; + + public List Payloads { get; } + + public Dictionary> MappedPayloads + { + get + { + if (mappedPayloads_ == null) + { + mappedPayloads_ = new Dictionary>(); + foreach (var p in Payloads) + { + if (!mappedPayloads_.ContainsKey(p.Type)) + { + mappedPayloads_[p.Type] = new List(); + } + mappedPayloads_[p.Type].Add(p); + } + } + + return mappedPayloads_; + } + } + + public SeString(List payloads) + { + Payloads = payloads; + } + + /// + /// Helper function to get all raw text from a message as a single joined string + /// + /// + /// All the raw text from the contained payloads, joined into a single string + /// + public string TextValue + { + get { + var sb = new StringBuilder(); + foreach (var p in Payloads) + { + if (p.Type == PayloadType.RawText) + { + sb.Append(((TextPayload)p).Text); + } + } + + return sb.ToString(); + } + } + + /// + /// Parse an array of bytes to a SeString. + /// + /// + /// + public static SeString Parse(byte[] bytes) + { + var payloads = new List(); + + using (var stream = new MemoryStream(bytes)) { + using var reader = new BinaryReader(stream); + + while (stream.Position < bytes.Length) + { + var payload = Payload.Process(reader); + if (payload != null) + payloads.Add(payload); + } + } + + return new SeString(payloads); + } + + /// + /// Encode a parsed/created SeString to an array of bytes, to be used for injection. + /// + /// + /// The bytes of the message. + public static byte[] Encode(List payloads) + { + var messageBytes = new List(); + foreach (var p in payloads) + { + messageBytes.AddRange(p.Encode()); + } + + return messageBytes.ToArray(); + } + } +} diff --git a/Dalamud/Game/ChatHandlers.cs b/Dalamud/Game/ChatHandlers.cs index 1bdfd9552..5420bfebc 100644 --- a/Dalamud/Game/ChatHandlers.cs +++ b/Dalamud/Game/ChatHandlers.cs @@ -7,6 +7,8 @@ using System.Text; using System.Text.RegularExpressions; using System.Threading.Tasks; using Dalamud.Game.Chat; +using Dalamud.Game.Chat.SeStringHandling; +using Dalamud.Game.Chat.SeStringHandling.Payloads; using Dalamud.Game.Internal.Libc; using Serilog; @@ -149,10 +151,16 @@ namespace Dalamud.Game { var itemInfo = matchInfo.Groups["item"]; if (!itemInfo.Success) continue; - //var itemName = SeString.Parse(itemInfo.Value).Output; - var (itemId, isHQ) = (ValueTuple)(SeString.Parse(message.RawData).Payloads[0].Param1); - Log.Debug($"Probable retainer sale: {message}, decoded item {itemId}, HQ {isHQ}"); + var itemLink = + SeString.Parse(message.RawData).Payloads.First(x => x.Type == PayloadType.Item) as ItemPayload; + + if (itemLink == null) { + Log.Error("itemLink was null. Msg: {0}", BitConverter.ToString(message.RawData)); + break; + } + + Log.Debug($"Probable retainer sale: {message}, decoded item {itemLink.ItemId}, HQ {itemLink.IsHQ}"); int itemValue = 0; var valueInfo = matchInfo.Groups["value"]; @@ -160,16 +168,16 @@ namespace Dalamud.Game { if (!valueInfo.Success || !int.TryParse(valueInfo.Value.Replace(",", "").Replace(".", ""), out itemValue)) continue; - Task.Run(() => this.dalamud.BotManager.ProcessRetainerSale(itemId, itemValue, isHQ)); + Task.Run(() => this.dalamud.BotManager.ProcessRetainerSale(itemLink.ItemId, itemValue, itemLink.IsHQ)); break; } } + var messageCopy = message; + var senderCopy = sender; + this.dalamud.BotManager.ProcessChatMessage(type, messageCopy, senderCopy); - Task.Run(() => this.dalamud.BotManager.ProcessChatMessage(type, messageVal, senderVal).GetAwaiter() - .GetResult()); - - + // Handle all of this with SeString some day if ((this.HandledChatTypeColors.ContainsKey(type) || type == XivChatType.Say || type == XivChatType.Shout || type == XivChatType.Alliance || type == XivChatType.TellOutgoing || type == XivChatType.Yell) && !message.Value.Contains((char)0x02)) { var italicsStart = message.Value.IndexOf("*");