diff --git a/Dalamud/Game/Chat/SeString.cs b/Dalamud/Game/Chat/SeString.cs index 8b777ddd5..21203baa0 100644 --- a/Dalamud/Game/Chat/SeString.cs +++ b/Dalamud/Game/Chat/SeString.cs @@ -14,29 +14,42 @@ namespace Dalamud.Game.Chat { 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(string input) { + public static (string Output, List Payloads) Parse(byte[] bytes) + { var output = new List(); var payloads = new List(); - var bytes = Encoding.UTF8.GetBytes(input); using (var stream = new MemoryStream(bytes)) - using (var reader = new BinaryReader(stream)) { - while (stream.Position < bytes.Length) { + 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); + 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(); @@ -54,11 +67,28 @@ namespace Dalamud.Game.Chat { switch ((SeStringPayloadType) type) { case SeStringPayloadType.PlayerLink: - if (payload[0] == (byte) PlayerLinkType.ItemLink) - payloads.Add(new SeStringPayloadContainer { + 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 = BitConverter.ToInt16(payload, 4) + Param1 = (itemId, isHQ) }); + } + break; } } diff --git a/Dalamud/Game/Chat/XivChatType.cs b/Dalamud/Game/Chat/XivChatType.cs index a52584153..1ecb16985 100644 --- a/Dalamud/Game/Chat/XivChatType.cs +++ b/Dalamud/Game/Chat/XivChatType.cs @@ -82,6 +82,8 @@ namespace Dalamud.Game.Chat { Echo = 56, SystemError = 58, GatheringSystemMessage = 60, + // not sure if this is used for anything else + RetainerSale = 71, [XivChatTypeInfo("Crossworld Linkshell 2", "cw2", 0xFF1E90FF)] CrossLinkShell2 = 101, diff --git a/Dalamud/Game/ChatHandlers.cs b/Dalamud/Game/ChatHandlers.cs index 5512a5c30..dc7f8e6c9 100644 --- a/Dalamud/Game/ChatHandlers.cs +++ b/Dalamud/Game/ChatHandlers.cs @@ -1,5 +1,5 @@ +using System; using System.Collections.Generic; -using System.Diagnostics; using System.Drawing; using System.Linq; using System.Reflection; @@ -54,6 +54,34 @@ namespace Dalamud.Game { new Regex(@"(http|ftp|https)://([\w_-]+(?:(?:\.[\w_-]+)+))([\w.,@?^=%&:/~+#-]*[\w@?^=%&/~+#-])?", RegexOptions.Compiled); + private readonly Dictionary retainerSaleRegexes = new Dictionary() { + { + ClientLanguage.Japanese, new Regex[] { + new Regex(@"^(?:.+)マーケットに(?[\d,.]+)ギルで出品した(?.*)×(?[\d,.]+)が売れ、(?[\d,.]+)ギルを入手しました。$", RegexOptions.Compiled), + new Regex(@"^(?:.+)マーケットに(?[\d,.]+)ギルで出品した(?.*)が売れ、(?[\d,.]+)ギルを入手しました。$", RegexOptions.Compiled) + } + }, + { + ClientLanguage.English, new Regex[] + { + new Regex(@"^(?.+) you put up for sale in the (?:.+) markets (?:have|has) sold for (?[\d,.]+) gil \(after fees\)\.$", RegexOptions.Compiled) + } + }, + { + ClientLanguage.German, new Regex[] + { + new Regex(@"^Dein Gehilfe hat (?.+) auf dem Markt von (?:.+) für (?[\d,.]+) Gil verkauft\.$", RegexOptions.Compiled), + new Regex(@"^Dein Gehilfe hat (?.+) auf dem Markt von (?:.+) verkauft und (?[\d,.]+) Gil erhalten\.$", RegexOptions.Compiled) + } + }, + { + ClientLanguage.French, new Regex[] + { + new Regex(@"^Un servant a vendu (?.+) pour (?[\d,.]+) gil à (?:.+)\.$", RegexOptions.Compiled) + } + } + }; + private bool hasSeenLoadingMsg; public ChatHandlers(Dalamud dalamud) { @@ -65,8 +93,8 @@ namespace Dalamud.Game { public string LastLink { get; private set; } - private void ChatOnOnChatMessage(XivChatType type, uint senderId, string sender, ref string message, - ref bool isHandled) { + private void ChatOnOnChatMessage(XivChatType type, uint senderId, string sender, byte[] rawMessage, + ref string message, ref bool isHandled) { if (type == XivChatType.Notice && !this.hasSeenLoadingMsg) { this.dalamud.Framework.Gui.Chat.Print($"XIVLauncher in-game addon v{Assembly.GetAssembly(typeof(ChatHandlers)).GetName().Version} loaded."); @@ -96,8 +124,36 @@ namespace Dalamud.Game { return; } + if (type == XivChatType.RetainerSale) + { + foreach (var regex in retainerSaleRegexes[dalamud.StartInfo.Language]) + { + var matchInfo = regex.Match(message); + + // we no longer really need to do/validate the item matching since we read the id from the byte array + // but we'd be checking the main match anyway + var itemInfo = matchInfo.Groups["item"]; + if (!itemInfo.Success) + continue; + //var itemName = SeString.Parse(itemInfo.Value).Output; + var (itemId, isHQ) = (ValueTuple)(SeString.Parse(rawMessage).Payloads[0].Param1); + + Log.Debug($"Probable retainer sale: {message}, decoded item {itemId}, HQ {isHQ}"); + + int itemValue = 0; + var valueInfo = matchInfo.Groups["value"]; + // not sure if using a culture here would work correctly, so just strip symbols instead + if (!valueInfo.Success || !int.TryParse(valueInfo.Value.Replace(",", "").Replace(".", ""), out itemValue)) + continue; + + Task.Run(() => this.dalamud.BotManager.ProcessRetainerSale(itemId, itemValue, isHQ)); + break; + } + } + + Task.Run(() => this.dalamud.BotManager.ProcessChatMessage(type, originalMessage, sender).GetAwaiter() - .GetResult()); + .GetResult()); if ((this.HandledChatTypeColors.ContainsKey(type) || type == XivChatType.Say || type == XivChatType.Shout || diff --git a/Dalamud/Game/Command/CommandManager.cs b/Dalamud/Game/Command/CommandManager.cs index b7b590ac4..023a0d061 100644 --- a/Dalamud/Game/Command/CommandManager.cs +++ b/Dalamud/Game/Command/CommandManager.cs @@ -50,8 +50,8 @@ namespace Dalamud.Game.Command { dalamud.Framework.Gui.Chat.OnChatMessage += OnChatMessage; } - private void OnChatMessage(XivChatType type, uint senderId, string sender, ref string message, - ref bool isHandled) { + private void OnChatMessage(XivChatType type, uint senderId, string sender, byte[] rawMessage, + ref string message, ref bool isHandled) { if (type == XivChatType.GatheringSystemMessage && senderId == 0) { var cmdMatch = this.CommandRegex.Match(message).Groups["command"]; if (cmdMatch.Success) { diff --git a/Dalamud/Game/Internal/Gui/ChatGui.cs b/Dalamud/Game/Internal/Gui/ChatGui.cs index 4d254a71c..a5cbef50c 100644 --- a/Dalamud/Game/Internal/Gui/ChatGui.cs +++ b/Dalamud/Game/Internal/Gui/ChatGui.cs @@ -14,7 +14,7 @@ namespace Dalamud.Game.Internal.Gui { IntPtr message, uint senderId, IntPtr parameter); - public delegate void OnMessageDelegate(XivChatType type, uint senderId, string sender, ref string message, + public delegate void OnMessageDelegate(XivChatType type, uint senderId, string sender, byte[] rawMessage, ref string message, ref bool isHandled); @@ -84,16 +84,19 @@ namespace Dalamud.Game.Internal.Gui { IntPtr retVal = IntPtr.Zero; try { + ByteWrapper messageBytes = new ByteWrapper(); + var senderName = StdString.ReadFromPointer(pSenderName); - var message = StdString.ReadFromPointer(pMessage); + var message = StdString.ReadFromPointer(pMessage, messageBytes); Log.Debug($"HandlePrintMessageDetour {manager} - [{chattype}] [{BitConverter.ToString(Encoding.UTF8.GetBytes(message)).Replace("-", " ")}] {message} from {senderName}"); + // Log.Debug($"Got message bytes {BitConverter.ToString(messageBytes.Bytes).Replace("-", " ")}"); var originalMessage = string.Copy(message); // Call events var isHandled = false; - OnChatMessage?.Invoke(chattype, senderid, senderName, ref message, ref isHandled); + OnChatMessage?.Invoke(chattype, senderid, senderName, messageBytes.Bytes, ref message, ref isHandled); var messagePtr = pMessage; OwnedStdString allocatedString = null; diff --git a/Dalamud/Game/Internal/Libc/StdString.cs b/Dalamud/Game/Internal/Libc/StdString.cs index c3d6f315b..6ced3ffa3 100644 --- a/Dalamud/Game/Internal/Libc/StdString.cs +++ b/Dalamud/Game/Internal/Libc/StdString.cs @@ -1,13 +1,14 @@ -using System; +using System; using System.Runtime.InteropServices; using System.Text; +using Serilog; namespace Dalamud.Game.Internal.Libc { /// /// Interation with std::string /// public static class StdString { - public static string ReadFromPointer(IntPtr cstring) { + public static string ReadFromPointer(IntPtr cstring, ByteWrapper bytes = null) { unsafe { if (cstring == IntPtr.Zero) { throw new ArgumentNullException(nameof(cstring)); @@ -25,9 +26,27 @@ namespace Dalamud.Game.Internal.Libc { while (*(pInner + count) != 0) { count += 1; } + + // raw copy if requested, as the string conversion returned from this function is lossy + if (bytes != null) + { + bytes.Bytes = new byte[count]; + for (int i = 0; i < count; i++) + { + bytes.Bytes[i] = (byte)pInner[i]; + } + } return new string(pInner, 0, count, Encoding.UTF8); } } } + + /// + /// Wrapper so that we can use an optional byte[] as a parameter + /// + public class ByteWrapper + { + public byte[] Bytes { get; set; } = null; + } }