mirror of
https://github.com/goatcorp/Dalamud.git
synced 2025-12-12 18:27:23 +01:00
feat: improve chat string parsing - thanks meli!
This commit is contained in:
parent
02a9c64a78
commit
0c4febb680
10 changed files with 690 additions and 191 deletions
|
|
@ -1,11 +1,15 @@
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.IO.Ports;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Text.RegularExpressions;
|
using System.Text.RegularExpressions;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Dalamud.Game.Chat;
|
using Dalamud.Game.Chat;
|
||||||
|
using Dalamud.Game.Chat.SeStringHandling;
|
||||||
|
using Dalamud.Game.Chat.SeStringHandling.Payloads;
|
||||||
|
using Dalamud.Game.Internal.Libc;
|
||||||
using Discord;
|
using Discord;
|
||||||
using Discord.WebSocket;
|
using Discord.WebSocket;
|
||||||
using Newtonsoft.Json.Linq;
|
using Newtonsoft.Json.Linq;
|
||||||
|
|
@ -157,12 +161,11 @@ namespace Dalamud.DiscordBot {
|
||||||
await channel.SendMessageAsync(embed: embedBuilder.Build());
|
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
|
// Special case for outgoing tells, these should be sent under Incoming tells
|
||||||
var wasOutgoingTell = false;
|
var wasOutgoingTell = false;
|
||||||
if (type == XivChatType.TellOutgoing) {
|
if (type == XivChatType.TellOutgoing) {
|
||||||
type = XivChatType.TellIncoming;
|
type = XivChatType.TellIncoming;
|
||||||
sender = this.dalamud.ClientState.LocalPlayer.Name;
|
|
||||||
wasOutgoingTell = true;
|
wasOutgoingTell = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -173,32 +176,34 @@ namespace Dalamud.DiscordBot {
|
||||||
return;
|
return;
|
||||||
|
|
||||||
var chatTypeDetail = type.GetDetails();
|
var chatTypeDetail = type.GetDetails();
|
||||||
|
|
||||||
var channels = chatTypeConfigs.Select(c => GetChannel(c.Channel).GetAwaiter().GetResult());
|
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)
|
if (playerLink == null) {
|
||||||
world = this.dalamud.ClientState.LocalPlayer.CurrentWorld.Name;
|
Log.Error("playerLink was null. Sender: {0}", BitConverter.ToString(sender.RawData));
|
||||||
|
|
||||||
if (senderSplit.Length == 2) {
|
senderName = wasOutgoingTell ? this.dalamud.ClientState.LocalPlayer.Name : parsedSender.TextValue;
|
||||||
world = senderSplit[1];
|
senderWorld = this.dalamud.ClientState.LocalPlayer.HomeWorld.Name;
|
||||||
sender = senderSplit[0];
|
} else {
|
||||||
|
playerLink.Resolve();
|
||||||
|
|
||||||
|
senderName = wasOutgoingTell ? this.dalamud.ClientState.LocalPlayer.Name : playerLink.PlayerName;
|
||||||
|
senderWorld = playerLink.ServerName;
|
||||||
}
|
}
|
||||||
|
|
||||||
sender = SeString.Parse(sender).Output;
|
var rawMessage = SeString.Parse(message.RawData).TextValue;
|
||||||
message = SeString.Parse(message).Output;
|
|
||||||
|
|
||||||
sender = RemoveAllNonLanguageCharacters(sender);
|
var avatarUrl = string.Empty;
|
||||||
|
var lodestoneId = string.Empty;
|
||||||
var avatarUrl = "";
|
|
||||||
var lodestoneId = "";
|
|
||||||
|
|
||||||
if (!this.config.DisableEmbeds) {
|
if (!this.config.DisableEmbeds) {
|
||||||
var searchResult = await GetCharacterInfo(sender, world);
|
var searchResult = await GetCharacterInfo(senderName, senderWorld);
|
||||||
|
|
||||||
lodestoneId = searchResult.LodestoneId;
|
lodestoneId = searchResult.LodestoneId;
|
||||||
avatarUrl = searchResult.AvatarUrl;
|
avatarUrl = searchResult.AvatarUrl;
|
||||||
|
|
@ -208,9 +213,9 @@ namespace Dalamud.DiscordBot {
|
||||||
|
|
||||||
var name = wasOutgoingTell
|
var name = wasOutgoingTell
|
||||||
? "You"
|
? "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++) {
|
for (var chatTypeIndex = 0; chatTypeIndex < chatTypeConfigs.Count(); chatTypeIndex++) {
|
||||||
if (!this.config.DisableEmbeds) {
|
if (!this.config.DisableEmbeds) {
|
||||||
|
|
@ -222,7 +227,7 @@ namespace Dalamud.DiscordBot {
|
||||||
Name = name,
|
Name = name,
|
||||||
Url = !string.IsNullOrEmpty(lodestoneId) ? "https://eu.finalfantasyxiv.com/lodestone/character/" + lodestoneId : null
|
Url = !string.IsNullOrEmpty(lodestoneId) ? "https://eu.finalfantasyxiv.com/lodestone/character/" + lodestoneId : null
|
||||||
},
|
},
|
||||||
Description = message,
|
Description = rawMessage,
|
||||||
Timestamp = DateTimeOffset.Now,
|
Timestamp = DateTimeOffset.Now,
|
||||||
Footer = new EmbedFooterBuilder { Text = type.GetDetails().FancyName },
|
Footer = new EmbedFooterBuilder { Text = type.GetDetails().FancyName },
|
||||||
Color = new Color((uint)(chatTypeConfigs.ElementAt(chatTypeIndex).Color & 0xFFFFFF))
|
Color = new Color((uint)(chatTypeConfigs.ElementAt(chatTypeIndex).Color & 0xFFFFFF))
|
||||||
|
|
@ -253,7 +258,7 @@ namespace Dalamud.DiscordBot {
|
||||||
|
|
||||||
await channels.ElementAt(chatTypeIndex).SendMessageAsync(embed: embedBuilder.Build());
|
await channels.ElementAt(chatTypeIndex).SendMessageAsync(embed: embedBuilder.Build());
|
||||||
} else {
|
} else {
|
||||||
var simpleMessage = $"{name}: {message}";
|
var simpleMessage = $"{name}: {rawMessage}";
|
||||||
|
|
||||||
if (this.config.CheckForDuplicateMessages) {
|
if (this.config.CheckForDuplicateMessages) {
|
||||||
var recentMsg = this.recentMessages.FirstOrDefault(
|
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();
|
return await this.socketClient.GetUser(channelConfig.ChannelId).GetOrCreateDMChannelAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
private string RemoveAllNonLanguageCharacters(string input) {
|
|
||||||
return Regex.Replace(input, @"[^\p{L} ']", "");
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Dispose() {
|
public void Dispose() {
|
||||||
this.socketClient.LogoutAsync().GetAwaiter().GetResult();
|
this.socketClient.LogoutAsync().GetAwaiter().GetResult();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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<SeStringPayloadContainer> Payloads) Parse(byte[] bytes)
|
|
||||||
{
|
|
||||||
var output = new List<byte>();
|
|
||||||
var payloads = new List<SeStringPayloadContainer>();
|
|
||||||
|
|
||||||
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<SeStringPayloadContainer> Payloads) Parse(string input) {
|
|
||||||
var bytes = Encoding.UTF8.GetBytes(input);
|
|
||||||
return Parse(bytes);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void ProcessPacket(BinaryReader reader, List<byte> output,
|
|
||||||
List<SeStringPayloadContainer> 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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
221
Dalamud/Game/Chat/SeStringHandling/Payload.cs
Normal file
221
Dalamud/Game/Chat/SeStringHandling/Payload.cs
Normal file
|
|
@ -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
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// This class represents a parsed SeString payload.
|
||||||
|
/// </summary>
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
31
Dalamud/Game/Chat/SeStringHandling/PayloadType.cs
Normal file
31
Dalamud/Game/Chat/SeStringHandling/PayloadType.cs
Normal file
|
|
@ -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
|
||||||
|
{
|
||||||
|
/// <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
|
||||||
|
}
|
||||||
|
}
|
||||||
81
Dalamud/Game/Chat/SeStringHandling/Payloads/ItemPayload.cs
Normal file
81
Dalamud/Game/Chat/SeStringHandling/Payloads/ItemPayload.cs
Normal file
|
|
@ -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<byte>()
|
||||||
|
{
|
||||||
|
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));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
85
Dalamud/Game/Chat/SeStringHandling/Payloads/PlayerPayload.cs
Normal file
85
Dalamud/Game/Chat/SeStringHandling/Payloads/PlayerPayload.cs
Normal file
|
|
@ -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<byte>()
|
||||||
|
{
|
||||||
|
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));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
64
Dalamud/Game/Chat/SeStringHandling/Payloads/StatusPayload.cs
Normal file
64
Dalamud/Game/Chat/SeStringHandling/Payloads/StatusPayload.cs
Normal file
|
|
@ -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<byte>()
|
||||||
|
{
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
58
Dalamud/Game/Chat/SeStringHandling/Payloads/TextPayload.cs
Normal file
58
Dalamud/Game/Chat/SeStringHandling/Payloads/TextPayload.cs
Normal file
|
|
@ -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<byte>();
|
||||||
|
|
||||||
|
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());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
107
Dalamud/Game/Chat/SeStringHandling/SeString.cs
Normal file
107
Dalamud/Game/Chat/SeStringHandling/SeString.cs
Normal file
|
|
@ -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
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// This class represents a parsed SeString.
|
||||||
|
/// </summary>
|
||||||
|
public class SeString
|
||||||
|
{
|
||||||
|
private Dictionary<PayloadType, List<Payload>> mappedPayloads_ = null;
|
||||||
|
|
||||||
|
public List<Payload> Payloads { get; }
|
||||||
|
|
||||||
|
public Dictionary<PayloadType, List<Payload>> MappedPayloads
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
if (mappedPayloads_ == null)
|
||||||
|
{
|
||||||
|
mappedPayloads_ = new Dictionary<PayloadType, List<Payload>>();
|
||||||
|
foreach (var p in Payloads)
|
||||||
|
{
|
||||||
|
if (!mappedPayloads_.ContainsKey(p.Type))
|
||||||
|
{
|
||||||
|
mappedPayloads_[p.Type] = new List<Payload>();
|
||||||
|
}
|
||||||
|
mappedPayloads_[p.Type].Add(p);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return mappedPayloads_;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public SeString(List<Payload> payloads)
|
||||||
|
{
|
||||||
|
Payloads = payloads;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <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 {
|
||||||
|
var sb = new StringBuilder();
|
||||||
|
foreach (var p in Payloads)
|
||||||
|
{
|
||||||
|
if (p.Type == PayloadType.RawText)
|
||||||
|
{
|
||||||
|
sb.Append(((TextPayload)p).Text);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return sb.ToString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Parse an array of bytes to a SeString.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="bytes"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public static 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.Process(reader);
|
||||||
|
if (payload != null)
|
||||||
|
payloads.Add(payload);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return new SeString(payloads);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Encode a parsed/created SeString to an array of bytes, to be used for injection.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="payloads"></param>
|
||||||
|
/// <returns>The bytes of the message.</returns>
|
||||||
|
public static byte[] Encode(List<Payload> payloads)
|
||||||
|
{
|
||||||
|
var messageBytes = new List<byte>();
|
||||||
|
foreach (var p in payloads)
|
||||||
|
{
|
||||||
|
messageBytes.AddRange(p.Encode());
|
||||||
|
}
|
||||||
|
|
||||||
|
return messageBytes.ToArray();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -7,6 +7,8 @@ using System.Text;
|
||||||
using System.Text.RegularExpressions;
|
using System.Text.RegularExpressions;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Dalamud.Game.Chat;
|
using Dalamud.Game.Chat;
|
||||||
|
using Dalamud.Game.Chat.SeStringHandling;
|
||||||
|
using Dalamud.Game.Chat.SeStringHandling.Payloads;
|
||||||
using Dalamud.Game.Internal.Libc;
|
using Dalamud.Game.Internal.Libc;
|
||||||
using Serilog;
|
using Serilog;
|
||||||
|
|
||||||
|
|
@ -149,10 +151,16 @@ namespace Dalamud.Game {
|
||||||
var itemInfo = matchInfo.Groups["item"];
|
var itemInfo = matchInfo.Groups["item"];
|
||||||
if (!itemInfo.Success)
|
if (!itemInfo.Success)
|
||||||
continue;
|
continue;
|
||||||
//var itemName = SeString.Parse(itemInfo.Value).Output;
|
|
||||||
var (itemId, isHQ) = (ValueTuple<int, bool>)(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;
|
int itemValue = 0;
|
||||||
var valueInfo = matchInfo.Groups["value"];
|
var valueInfo = matchInfo.Groups["value"];
|
||||||
|
|
@ -160,16 +168,16 @@ namespace Dalamud.Game {
|
||||||
if (!valueInfo.Success || !int.TryParse(valueInfo.Value.Replace(",", "").Replace(".", ""), out itemValue))
|
if (!valueInfo.Success || !int.TryParse(valueInfo.Value.Replace(",", "").Replace(".", ""), out itemValue))
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
Task.Run(() => this.dalamud.BotManager.ProcessRetainerSale(itemId, itemValue, isHQ));
|
Task.Run(() => this.dalamud.BotManager.ProcessRetainerSale(itemLink.ItemId, itemValue, itemLink.IsHQ));
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var messageCopy = message;
|
||||||
|
var senderCopy = sender;
|
||||||
|
this.dalamud.BotManager.ProcessChatMessage(type, messageCopy, senderCopy);
|
||||||
|
|
||||||
Task.Run(() => this.dalamud.BotManager.ProcessChatMessage(type, messageVal, senderVal).GetAwaiter()
|
// Handle all of this with SeString some day
|
||||||
.GetResult());
|
|
||||||
|
|
||||||
|
|
||||||
if ((this.HandledChatTypeColors.ContainsKey(type) || type == XivChatType.Say || type == XivChatType.Shout ||
|
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)) {
|
type == XivChatType.Alliance || type == XivChatType.TellOutgoing || type == XivChatType.Yell) && !message.Value.Contains((char)0x02)) {
|
||||||
var italicsStart = message.Value.IndexOf("*");
|
var italicsStart = message.Value.IndexOf("*");
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue