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.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();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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.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<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;
|
||||
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("*");
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue