From 1894d579a8ec77590d839607f326983967a99d79 Mon Sep 17 00:00:00 2001 From: meli <57847713+ff-meli@users.noreply.github.com> Date: Tue, 31 Mar 2020 09:34:47 -0700 Subject: [PATCH 1/5] initial map link payload implementation. Generally works, but at least needs a better way to handle lumina DI --- Dalamud/Dalamud.cs | 3 + Dalamud/Game/Chat/SeStringHandling/Payload.cs | 69 +++++++-- .../Game/Chat/SeStringHandling/PayloadType.cs | 4 + .../SeStringHandling/Payloads/ItemPayload.cs | 2 +- .../Payloads/MapLinkPayload.cs | 143 ++++++++++++++++++ .../Game/Chat/SeStringHandling/SeString.cs | 5 +- 6 files changed, 210 insertions(+), 16 deletions(-) create mode 100644 Dalamud/Game/Chat/SeStringHandling/Payloads/MapLinkPayload.cs diff --git a/Dalamud/Dalamud.cs b/Dalamud/Dalamud.cs index 3ed3b2ef8..5dcc85c8e 100644 --- a/Dalamud/Dalamud.cs +++ b/Dalamud/Dalamud.cs @@ -88,6 +88,9 @@ namespace Dalamud { this.Data = new DataManager(this.StartInfo.Language); this.Data.Initialize(); + // FIXME: need a better way to get this into the string payloads + Game.Chat.SeStringHandling.SeString.DataResolver = this.Data; + this.ClientState = new ClientState(this, info, this.SigScanner); this.BotManager = new DiscordBotManager(this, this.Configuration.DiscordFeatureConfig); diff --git a/Dalamud/Game/Chat/SeStringHandling/Payload.cs b/Dalamud/Game/Chat/SeStringHandling/Payload.cs index fbbaf2ab8..d0c17ecf8 100644 --- a/Dalamud/Game/Chat/SeStringHandling/Payload.cs +++ b/Dalamud/Game/Chat/SeStringHandling/Payload.cs @@ -4,6 +4,7 @@ using System.IO; using System.Linq; using System.Text; using System.Threading.Tasks; +using Dalamud.Data; using Dalamud.Game.Chat.SeStringHandling.Payloads; using Serilog; @@ -16,22 +17,32 @@ namespace Dalamud.Game.Chat.SeStringHandling { public abstract PayloadType Type { get; } + protected DataManager dataResolver; + public abstract void Resolve(); public abstract byte[] Encode(); protected abstract void ProcessChunkImpl(BinaryReader reader, long endOfStream); - public static Payload Process(BinaryReader reader) + public static Payload Process(BinaryReader reader, DataManager dataResolver) { + Payload payload = null; if ((byte)reader.PeekChar() != START_BYTE) { - return ProcessText(reader); + payload = ProcessText(reader); } else { - return ProcessChunk(reader); + payload = ProcessChunk(reader); } + + if (payload != null) + { + payload.dataResolver = dataResolver; + } + + return payload; } private static Payload ProcessChunk(BinaryReader reader) @@ -59,6 +70,10 @@ namespace Dalamud.Game.Chat.SeStringHandling payload = new ItemPayload(); break; + case EmbeddedInfoType.MapPositionLink: + payload = new MapLinkPayload(); + break; + case EmbeddedInfoType.Status: payload = new StatusPayload(); break; @@ -74,6 +89,7 @@ namespace Dalamud.Game.Chat.SeStringHandling } } break; + default: Log.Verbose("Unhandled SeStringChunkType: {0}", chunkType); payload = new RawPayload((byte)chunkType); @@ -111,6 +127,7 @@ namespace Dalamud.Game.Chat.SeStringHandling { PlayerName = 0x01, ItemLink = 0x03, + MapPositionLink = 0x04, Status = 0x09, LinkTerminator = 0xCF // not clear but seems to always follow a link @@ -118,10 +135,14 @@ namespace Dalamud.Game.Chat.SeStringHandling protected enum IntegerType { + // used as an internal marker; sometimes single bytes are bare with no marker at all + None = 0, + Byte = 0xF0, ByteTimes256 = 0xF1, Int16 = 0xF2, - Int16Plus1Million = 0xF6, + Int16Packed = 0xF4, // seen in map links, seemingly 2 8-bit values packed into 2 bytes with only one marker + Int24Special = 0xF6, // unsure how different form Int24 - used for hq items that add 1 million, also used for normal 24-bit values in map links Int24 = 0xFA, Int32 = 0xFE } @@ -147,25 +168,22 @@ namespace Dalamud.Game.Chat.SeStringHandling { case IntegerType.Byte: return input.ReadByte(); + case IntegerType.ByteTimes256: return input.ReadByte() * 256; + case IntegerType.Int16: + // fallthrough - same logic + case IntegerType.Int16Packed: { 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.Int24Special: + // Fallthrough - same logic case IntegerType.Int24: { var v = 0; @@ -174,6 +192,7 @@ namespace Dalamud.Game.Chat.SeStringHandling v |= input.ReadByte(); return v; } + case IntegerType.Int32: { var v = 0; @@ -183,6 +202,7 @@ namespace Dalamud.Game.Chat.SeStringHandling v |= input.ReadByte(); return v; } + default: throw new NotSupportedException(); } @@ -220,6 +240,27 @@ namespace Dalamud.Game.Chat.SeStringHandling throw new NotSupportedException(); } + + protected static (int, int) GetPackedIntegers(BinaryReader input) + { + var value = (uint)GetInteger(input); + if (value > 0xFFFF) + { + return ((int)((value & 0xFFFF0000) >> 16), (int)(value & 0xFFFF)); + } + else if (value > 0xFF) + { + return ((int)((value & 0xFF00) >> 8), (int)(value & 0xFF)); + } + + // unsure if there are other cases, like "odd" pairings of 2+1 bytes etc + throw new NotSupportedException(); + } + + protected static byte[] MakePackedInteger(int val1, int val2) + { + return MakeInteger(val1).Concat(MakeInteger(val2)).ToArray(); + } #endregion } } diff --git a/Dalamud/Game/Chat/SeStringHandling/PayloadType.cs b/Dalamud/Game/Chat/SeStringHandling/PayloadType.cs index 6576e0524..77f99c54c 100644 --- a/Dalamud/Game/Chat/SeStringHandling/PayloadType.cs +++ b/Dalamud/Game/Chat/SeStringHandling/PayloadType.cs @@ -28,6 +28,10 @@ namespace Dalamud.Game.Chat.SeStringHandling /// RawText, /// + /// An SeString payload representing a map position link, such as from <flag> or <pos>. + /// + MapLink, + /// /// An SeString payload representing any data we don't handle. /// Unknown diff --git a/Dalamud/Game/Chat/SeStringHandling/Payloads/ItemPayload.cs b/Dalamud/Game/Chat/SeStringHandling/Payloads/ItemPayload.cs index 1a1dc0bb7..23b77c238 100644 --- a/Dalamud/Game/Chat/SeStringHandling/Payloads/ItemPayload.cs +++ b/Dalamud/Game/Chat/SeStringHandling/Payloads/ItemPayload.cs @@ -38,7 +38,7 @@ namespace Dalamud.Game.Chat.SeStringHandling.Payloads var idBytes = MakeInteger(actualItemId); bool hasName = !string.IsNullOrEmpty(ItemName); - var itemIdFlag = IsHQ ? IntegerType.Int16Plus1Million : IntegerType.Int16; + var itemIdFlag = IsHQ ? IntegerType.Int24Special : IntegerType.Int16; var chunkLen = idBytes.Length + 5; if (hasName) diff --git a/Dalamud/Game/Chat/SeStringHandling/Payloads/MapLinkPayload.cs b/Dalamud/Game/Chat/SeStringHandling/Payloads/MapLinkPayload.cs new file mode 100644 index 000000000..512a60a9a --- /dev/null +++ b/Dalamud/Game/Chat/SeStringHandling/Payloads/MapLinkPayload.cs @@ -0,0 +1,143 @@ +using Lumina.Excel.GeneratedSheets; +using System; +using System.Collections.Generic; +using System.IO; + +namespace Dalamud.Game.Chat.SeStringHandling.Payloads +{ + public class MapLinkPayload : Payload + { + public override PayloadType Type => PayloadType.MapLink; + + // pre-Resolve() values + public int TerritoryTypeId { get; set; } + public int MapId { get; set; } + public uint RawX { get; set; } + public uint RawY { get; set; } + + // Resolved values + // It might make sense to have Territory be an external type, that has assorted relevant info + public string Territory { get; private set; } + public string Zone { get; private set; } + public float XCoord { get; private set; } + public float YCoord { get; private set; } + // Z? + + public override byte[] Encode() + { + // TODO: for now we just encode the raw/internal values + // eventually we should allow creation using 'nice' values that then encode properly + + var packedTerritoryAndMapBytes = WrapPackedIntegerAndMarker(TerritoryTypeId, MapId); + var xBytes = WrapIntegerAndMarker((int)RawX); + var yBytes = WrapIntegerAndMarker((int)RawY); + + var chunkLen = 4 + packedTerritoryAndMapBytes.Length + xBytes.Length + yBytes.Length; + + var bytes = new List() + { + START_BYTE, + (byte)SeStringChunkType.Interactable, (byte)chunkLen, (byte)EmbeddedInfoType.MapPositionLink + }; + + bytes.AddRange(packedTerritoryAndMapBytes); + bytes.AddRange(xBytes); + bytes.AddRange(yBytes); + + // unk + bytes.AddRange(new byte[] { 0xFF, 0x01, END_BYTE }); + + return bytes.ToArray(); + } + + public override void Resolve() + { + if (string.IsNullOrEmpty(Territory)) + { + var terrRow = dataResolver.GetExcelSheet().GetRow(TerritoryTypeId); + Territory = dataResolver.GetExcelSheet().GetRow(terrRow.PlaceName).Name; + Zone = dataResolver.GetExcelSheet().GetRow(terrRow.PlaceNameZone).Name; + + var mapSizeFactor = dataResolver.GetExcelSheet().GetRow(MapId).SizeFactor; + XCoord = ConvertRawPositionToMapCoordinate(RawX, mapSizeFactor); + YCoord = ConvertRawPositionToMapCoordinate(RawY, mapSizeFactor); + } + } + + protected override void ProcessChunkImpl(BinaryReader reader, long endOfStream) + { + (TerritoryTypeId, MapId) = GetPackedIntegers(reader); + RawX = (uint)GetInteger(reader); + RawY = (uint)GetInteger(reader); + // the Z coordinate is never in this chunk, just the text (if applicable) + + // seems to always be FF 01 + reader.ReadBytes(2); + } + + #region ugliness + // from https://github.com/xivapi/ffxiv-datamining/blob/master/docs/MapCoordinates.md + // extra 1/1000 because that is how the network ints are done + private float ConvertRawPositionToMapCoordinate(uint pos, float scale) + { + var c = scale / 100.0f; + var scaledPos = (int)pos * c / 1000.0f; + + return ((41.0f / c) * ((scaledPos + 1024.0f) / 2048.0f)) + 1.0f; + } + + // Created as the inverse of ConvertRawPositionToMapCoordinate(), since no one seemed to have a version of that + private float ConvertMapCoordinateToRawPosition(float pos, float scale) + { + var c = scale / 100.0f; + + var scaledPos = ((((pos - 1.0f) * c / 41.0f) * 2048.0f) - 1024.0f) / c; + scaledPos *= 1000.0f; + + return (int)Math.Round(scaledPos); + } + #endregion + + private byte[] WrapIntegerAndMarker(int value) + { + var encodedValue = MakeInteger(value); + var marker = GetTypeForIntegerBytes_Custom(encodedValue, false); + + var bytes = new List(); + if ((IntegerType)marker != IntegerType.None) + { + bytes.Add(marker); + } + bytes.AddRange(encodedValue); + + return bytes.ToArray(); + } + + private byte[] WrapPackedIntegerAndMarker(int value1, int value2) + { + var encodedValue = MakePackedInteger(TerritoryTypeId, MapId); + var marker = GetTypeForIntegerBytes_Custom(encodedValue, true); + + var bytes = new List() { marker }; + bytes.AddRange(encodedValue); + + return bytes.ToArray(); + } + + // This seems entirely custom in various payload types + // I'm not really sure if it matters if we use one version of Int16 over another, for example + // but for now I try to match what incoming messages seem to use + private byte GetTypeForIntegerBytes_Custom(byte[] bytes, bool packed = false) + { + var type = bytes.Length switch + { + 3 => IntegerType.Int24Special, // used because seen in incoming data + 2 => packed ? IntegerType.Int16Packed : IntegerType.Int16, // packed bytes seem to get a different marker.. but not packed shorts + 1 => IntegerType.None, // single bytes seem to have no prefix at all here + _ => GetTypeForIntegerBytes(bytes) + }; + + return (byte)type; + } + } +} diff --git a/Dalamud/Game/Chat/SeStringHandling/SeString.cs b/Dalamud/Game/Chat/SeStringHandling/SeString.cs index 156612214..0bd9b3b51 100644 --- a/Dalamud/Game/Chat/SeStringHandling/SeString.cs +++ b/Dalamud/Game/Chat/SeStringHandling/SeString.cs @@ -4,6 +4,7 @@ using System.IO; using System.Linq; using System.Text; using System.Threading.Tasks; +using Dalamud.Data; using Dalamud.Game.Chat.SeStringHandling.Payloads; namespace Dalamud.Game.Chat.SeStringHandling @@ -13,6 +14,8 @@ namespace Dalamud.Game.Chat.SeStringHandling /// public class SeString { + public static DataManager DataResolver { get; set; } + public List Payloads { get; } public SeString(List payloads) @@ -56,7 +59,7 @@ namespace Dalamud.Game.Chat.SeStringHandling while (stream.Position < bytes.Length) { - var payload = Payload.Process(reader); + var payload = Payload.Process(reader, DataResolver); if (payload != null) payloads.Add(payload); } From 4c12b1dfb059c63e992093e91144235bf4b60030 Mon Sep 17 00:00:00 2001 From: meli <57847713+ff-meli@users.noreply.github.com> Date: Wed, 1 Apr 2020 20:06:19 -0700 Subject: [PATCH 2/5] Add UIGlow and UIForeground SeString payloads; refactor some handling of integer markers in payloads, to make encoding a bit simpler --- Dalamud/Game/Chat/SeStringHandling/Payload.cs | 70 +++++++++++++------ .../Game/Chat/SeStringHandling/PayloadType.cs | 8 +++ .../SeStringHandling/Payloads/ItemPayload.cs | 18 +++-- .../Payloads/StatusPayload.cs | 6 +- .../Payloads/UIForegroundPayload.cs | 58 +++++++++++++++ .../Payloads/UIGlowPayload.cs | 58 +++++++++++++++ 6 files changed, 186 insertions(+), 32 deletions(-) create mode 100644 Dalamud/Game/Chat/SeStringHandling/Payloads/UIForegroundPayload.cs create mode 100644 Dalamud/Game/Chat/SeStringHandling/Payloads/UIGlowPayload.cs diff --git a/Dalamud/Game/Chat/SeStringHandling/Payload.cs b/Dalamud/Game/Chat/SeStringHandling/Payload.cs index fbbaf2ab8..685b4c8a5 100644 --- a/Dalamud/Game/Chat/SeStringHandling/Payload.cs +++ b/Dalamud/Game/Chat/SeStringHandling/Payload.cs @@ -2,8 +2,6 @@ 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; @@ -74,6 +72,15 @@ namespace Dalamud.Game.Chat.SeStringHandling } } break; + + case SeStringChunkType.UIForeground: + payload = new UIForegroundPayload(); + break; + + case SeStringChunkType.UIGlow: + payload = new UIGlowPayload(); + break; + default: Log.Verbose("Unhandled SeStringChunkType: {0}", chunkType); payload = new RawPayload((byte)chunkType); @@ -104,7 +111,9 @@ namespace Dalamud.Game.Chat.SeStringHandling protected enum SeStringChunkType { - Interactable = 0x27 + Interactable = 0x27, + UIForeground = 0x48, + UIGlow = 0x49 } protected enum EmbeddedInfoType @@ -118,6 +127,9 @@ namespace Dalamud.Game.Chat.SeStringHandling protected enum IntegerType { + // Custom value indicating no marker at all + None = 0x0, + Byte = 0xF0, ByteTimes256 = 0xF1, Int16 = 0xF2, @@ -188,37 +200,49 @@ namespace Dalamud.Game.Chat.SeStringHandling } } - protected static byte[] MakeInteger(int value) + protected virtual byte[] MakeInteger(int value) { - // clearly the epitome of efficiency + // single-byte values below the marker values have no marker and have 1 added + if (value + 1 < (int)IntegerType.Byte) + { + value++; + return new byte[] { (byte)value }; + } var bytesPadded = BitConverter.GetBytes(value); Array.Reverse(bytesPadded); - return bytesPadded.SkipWhile(b => b == 0x00).ToArray(); + var shrunkValue = bytesPadded.SkipWhile(b => b == 0x00).ToArray(); + + var encodedNum = new List(); + + var marker = GetMarkerForIntegerBytes(shrunkValue); + if (marker != 0) + { + encodedNum.Add(marker); + } + + encodedNum.AddRange(shrunkValue); + + return encodedNum.ToArray(); } - protected static IntegerType GetTypeForIntegerBytes(byte[] bytes) + // This is only accurate in a very general sense + // Different payloads seem to use different default values for things + // So this should be overridden where necessary + protected virtual byte GetMarkerForIntegerBytes(byte[] bytes) { // not the most scientific, exists mainly for laziness - if (bytes.Length == 1) + var marker = bytes.Length switch { - 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; - } + 1 => IntegerType.Byte, + 2 => IntegerType.Int16, + 3 => IntegerType.Int24, + 4 => IntegerType.Int32, + _ => throw new NotSupportedException() + }; - throw new NotSupportedException(); + return (byte)marker; } #endregion } diff --git a/Dalamud/Game/Chat/SeStringHandling/PayloadType.cs b/Dalamud/Game/Chat/SeStringHandling/PayloadType.cs index 6576e0524..2a6107a76 100644 --- a/Dalamud/Game/Chat/SeStringHandling/PayloadType.cs +++ b/Dalamud/Game/Chat/SeStringHandling/PayloadType.cs @@ -28,6 +28,14 @@ namespace Dalamud.Game.Chat.SeStringHandling /// RawText, /// + /// An SeString payload representing a text foreground color. + /// + UIForeground, + /// + /// An SeString payload representing a text glow color. + /// + UIGlow, + /// /// An SeString payload representing any data we don't handle. /// Unknown diff --git a/Dalamud/Game/Chat/SeStringHandling/Payloads/ItemPayload.cs b/Dalamud/Game/Chat/SeStringHandling/Payloads/ItemPayload.cs index 1a1dc0bb7..7ebf6ba23 100644 --- a/Dalamud/Game/Chat/SeStringHandling/Payloads/ItemPayload.cs +++ b/Dalamud/Game/Chat/SeStringHandling/Payloads/ItemPayload.cs @@ -38,9 +38,7 @@ namespace Dalamud.Game.Chat.SeStringHandling.Payloads var idBytes = MakeInteger(actualItemId); bool hasName = !string.IsNullOrEmpty(ItemName); - var itemIdFlag = IsHQ ? IntegerType.Int16Plus1Million : IntegerType.Int16; - - var chunkLen = idBytes.Length + 5; + var chunkLen = idBytes.Length + 4; if (hasName) { // 1 additional unknown byte compared to the nameless version, 1 byte for the name length, and then the name itself @@ -54,8 +52,7 @@ namespace Dalamud.Game.Chat.SeStringHandling.Payloads var bytes = new List() { START_BYTE, - (byte)SeStringChunkType.Interactable, (byte)chunkLen, (byte)EmbeddedInfoType.ItemLink, - (byte)itemIdFlag + (byte)SeStringChunkType.Interactable, (byte)chunkLen, (byte)EmbeddedInfoType.ItemLink }; bytes.AddRange(idBytes); // unk @@ -122,5 +119,16 @@ namespace Dalamud.Game.Chat.SeStringHandling.Payloads ItemName = Encoding.UTF8.GetString(itemNameBytes); } } + + protected override byte GetMarkerForIntegerBytes(byte[] bytes) + { + // custom marker just for hq items? + if (bytes.Length == 3 && IsHQ) + { + return (byte)IntegerType.Int16Plus1Million; + } + + return base.GetMarkerForIntegerBytes(bytes); + } } } diff --git a/Dalamud/Game/Chat/SeStringHandling/Payloads/StatusPayload.cs b/Dalamud/Game/Chat/SeStringHandling/Payloads/StatusPayload.cs index 4169ac42a..410a044df 100644 --- a/Dalamud/Game/Chat/SeStringHandling/Payloads/StatusPayload.cs +++ b/Dalamud/Game/Chat/SeStringHandling/Payloads/StatusPayload.cs @@ -35,13 +35,11 @@ namespace Dalamud.Game.Chat.SeStringHandling.Payloads public override byte[] Encode() { var idBytes = MakeInteger(StatusId); - var idPrefix = GetTypeForIntegerBytes(idBytes); - var chunkLen = idBytes.Length + 8; + var chunkLen = idBytes.Length + 7; var bytes = new List() { - START_BYTE, (byte)SeStringChunkType.Interactable, (byte)chunkLen, (byte)EmbeddedInfoType.Status, - (byte)idPrefix + START_BYTE, (byte)SeStringChunkType.Interactable, (byte)chunkLen, (byte)EmbeddedInfoType.Status }; bytes.AddRange(idBytes); diff --git a/Dalamud/Game/Chat/SeStringHandling/Payloads/UIForegroundPayload.cs b/Dalamud/Game/Chat/SeStringHandling/Payloads/UIForegroundPayload.cs new file mode 100644 index 000000000..a9afebfa8 --- /dev/null +++ b/Dalamud/Game/Chat/SeStringHandling/Payloads/UIForegroundPayload.cs @@ -0,0 +1,58 @@ +using System; +using System.Collections.Generic; +using System.IO; + +namespace Dalamud.Game.Chat.SeStringHandling.Payloads +{ + public class UIForegroundPayload : Payload + { + public override PayloadType Type => PayloadType.UIForeground; + + public ushort RawColor { get; private set; } + + //public int Red { get; private set; } + //public int Green { get; private set; } + //public int Blue { get; private set; } + + public override byte[] Encode() + { + var colorBytes = MakeInteger(RawColor); + var chunkLen = colorBytes.Length + 1; + + var bytes = new List(new byte[] + { + START_BYTE, (byte)SeStringChunkType.UIForeground, (byte)chunkLen + }); + + bytes.AddRange(colorBytes); + bytes.Add(END_BYTE); + + return bytes.ToArray(); + } + + public override void Resolve() + { + // TODO: resolve color keys to hex colors via UIColor table + } + + public override string ToString() + { + return $"{Type} - RawColor: {RawColor}"; + } + + protected override void ProcessChunkImpl(BinaryReader reader, long endOfStream) + { + RawColor = (ushort)GetInteger(reader); + } + + protected override byte GetMarkerForIntegerBytes(byte[] bytes) + { + return bytes.Length switch + { + // a single byte of 0x01 is used to 'disable' color, and has no marker + 1 => (byte)IntegerType.None, + _ => base.GetMarkerForIntegerBytes(bytes) + }; + } + } +} diff --git a/Dalamud/Game/Chat/SeStringHandling/Payloads/UIGlowPayload.cs b/Dalamud/Game/Chat/SeStringHandling/Payloads/UIGlowPayload.cs new file mode 100644 index 000000000..3bc98fc6e --- /dev/null +++ b/Dalamud/Game/Chat/SeStringHandling/Payloads/UIGlowPayload.cs @@ -0,0 +1,58 @@ +using System; +using System.Collections.Generic; +using System.IO; + +namespace Dalamud.Game.Chat.SeStringHandling.Payloads +{ + public class UIGlowPayload : Payload + { + public override PayloadType Type => PayloadType.UIGlow; + + public ushort RawColor { get; private set; } + + //public int Red { get; private set; } + //public int Green { get; private set; } + //public int Blue { get; private set; } + + public override byte[] Encode() + { + var colorBytes = MakeInteger(RawColor); + var chunkLen = colorBytes.Length + 1; + + var bytes = new List(new byte[] + { + START_BYTE, (byte)SeStringChunkType.UIGlow, (byte)chunkLen + }); + + bytes.AddRange(colorBytes); + bytes.Add(END_BYTE); + + return bytes.ToArray(); + } + + public override void Resolve() + { + // TODO: resolve color keys to hex colors via UIColor table + } + + public override string ToString() + { + return $"{Type} - RawColor: {RawColor}"; + } + + protected override void ProcessChunkImpl(BinaryReader reader, long endOfStream) + { + RawColor = (ushort)GetInteger(reader); + } + + protected override byte GetMarkerForIntegerBytes(byte[] bytes) + { + return bytes.Length switch + { + // a single byte of 0x01 is used to 'disable' color, and has no marker + 1 => (byte)IntegerType.None, + _ => base.GetMarkerForIntegerBytes(bytes) + }; + } + } +} From 8fbac95b6b187e101358d57c8afcf73a6e091456 Mon Sep 17 00:00:00 2001 From: meli <57847713+ff-meli@users.noreply.github.com> Date: Thu, 2 Apr 2020 22:17:24 -0700 Subject: [PATCH 3/5] fixup string handling to use uints, since everything is realistically unsigned, and it can break encoding of actual 32-bit values otherwise --- Dalamud/Game/Chat/SeStringHandling/Payload.cs | 37 ++++++++++--------- .../SeStringHandling/Payloads/ItemPayload.cs | 8 ++-- .../Payloads/MapLinkPayload.cs | 12 +++--- .../Payloads/PlayerPayload.cs | 6 +-- .../Payloads/StatusPayload.cs | 4 +- Dalamud/Game/ChatHandlers.cs | 2 +- 6 files changed, 35 insertions(+), 34 deletions(-) diff --git a/Dalamud/Game/Chat/SeStringHandling/Payload.cs b/Dalamud/Game/Chat/SeStringHandling/Payload.cs index 665e6a73d..8f48c4174 100644 --- a/Dalamud/Game/Chat/SeStringHandling/Payload.cs +++ b/Dalamud/Game/Chat/SeStringHandling/Payload.cs @@ -105,8 +105,8 @@ namespace Dalamud.Game.Chat.SeStringHandling 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 + var readBytes = (uint)(reader.BaseStream.Position - packetStart); + reader.ReadBytes((int)(chunkLen - readBytes + 1)); // +1 for the END_BYTE marker return payload; } @@ -157,20 +157,20 @@ namespace Dalamud.Game.Chat.SeStringHandling // 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) + protected static uint GetInteger(BinaryReader input) { var t = input.ReadByte(); var type = (IntegerType)t; return GetInteger(input, type); } - private static int GetInteger(BinaryReader input, IntegerType type) + private static uint GetInteger(BinaryReader input, IntegerType type) { const byte ByteLengthCutoff = 0xF0; var t = (byte)type; if (t < ByteLengthCutoff) - return t - 1; + return (uint)(t - 1); switch (type) { @@ -178,7 +178,7 @@ namespace Dalamud.Game.Chat.SeStringHandling return input.ReadByte(); case IntegerType.ByteTimes256: - return input.ReadByte() * 256; + return input.ReadByte() * (uint)256; case IntegerType.Int16: // fallthrough - same logic @@ -187,7 +187,7 @@ namespace Dalamud.Game.Chat.SeStringHandling var v = 0; v |= input.ReadByte() << 8; v |= input.ReadByte(); - return v; + return (uint)v; } case IntegerType.Int24Special: @@ -198,7 +198,7 @@ namespace Dalamud.Game.Chat.SeStringHandling v |= input.ReadByte() << 16; v |= input.ReadByte() << 8; v |= input.ReadByte(); - return v; + return (uint)v; } case IntegerType.Int32: @@ -208,7 +208,7 @@ namespace Dalamud.Game.Chat.SeStringHandling v |= input.ReadByte() << 16; v |= input.ReadByte() << 8; v |= input.ReadByte(); - return v; + return (uint)v; } default: @@ -216,10 +216,10 @@ namespace Dalamud.Game.Chat.SeStringHandling } } - protected virtual byte[] MakeInteger(int value, bool withMarker = true) + protected virtual byte[] MakeInteger(uint value, bool withMarker = true, bool incrementSmallInts = true) // TODO: better way to handle this { // single-byte values below the marker values have no marker and have 1 added - if (value + 1 < (int)IntegerType.Byte) + if (incrementSmallInts && (value + 1 < (int)IntegerType.Byte)) { value++; return new byte[] { (byte)value }; @@ -266,9 +266,10 @@ namespace Dalamud.Game.Chat.SeStringHandling protected virtual byte GetMarkerForPackedIntegerBytes(byte[] bytes) { - // So far I've only ever seen this with 2 8-bit values packed into a short + // unsure if any 'strange' size groupings exist; only ever seen these var type = bytes.Length switch { + 4 => IntegerType.Int32, 2 => IntegerType.Int16Packed, _ => throw new NotSupportedException() }; @@ -276,25 +277,25 @@ namespace Dalamud.Game.Chat.SeStringHandling return (byte)type; } - protected (int, int) GetPackedIntegers(BinaryReader input) + protected (uint, uint) GetPackedIntegers(BinaryReader input) { - var value = (uint)GetInteger(input); + var value = GetInteger(input); if (value > 0xFFFF) { - return ((int)((value & 0xFFFF0000) >> 16), (int)(value & 0xFFFF)); + return ((uint)((value & 0xFFFF0000) >> 16), (uint)(value & 0xFFFF)); } else if (value > 0xFF) { - return ((int)((value & 0xFF00) >> 8), (int)(value & 0xFF)); + return ((uint)((value & 0xFF00) >> 8), (uint)(value & 0xFF)); } // unsure if there are other cases, like "odd" pairings of 2+1 bytes etc throw new NotSupportedException(); } - protected byte[] MakePackedInteger(int val1, int val2, bool withMarker = true) + protected byte[] MakePackedInteger(uint val1, uint val2, bool withMarker = true) { - var value = MakeInteger(val1, false).Concat(MakeInteger(val2, false)).ToArray(); + var value = MakeInteger(val1, false, false).Concat(MakeInteger(val2, false, false)).ToArray(); var valueBytes = new List(); if (withMarker) diff --git a/Dalamud/Game/Chat/SeStringHandling/Payloads/ItemPayload.cs b/Dalamud/Game/Chat/SeStringHandling/Payloads/ItemPayload.cs index 317214910..bc58d488e 100644 --- a/Dalamud/Game/Chat/SeStringHandling/Payloads/ItemPayload.cs +++ b/Dalamud/Game/Chat/SeStringHandling/Payloads/ItemPayload.cs @@ -11,13 +11,13 @@ namespace Dalamud.Game.Chat.SeStringHandling.Payloads { public override PayloadType Type => PayloadType.Item; - public int ItemId { get; private set; } + public uint ItemId { get; private set; } public string ItemName { get; private set; } = string.Empty; public bool IsHQ { get; private set; } = false; public ItemPayload() { } - public ItemPayload(int itemId, bool isHQ) + public ItemPayload(uint itemId, bool isHQ) { ItemId = itemId; IsHQ = isHQ; @@ -27,7 +27,7 @@ namespace Dalamud.Game.Chat.SeStringHandling.Payloads { if (string.IsNullOrEmpty(ItemName)) { - dynamic item = XivApi.GetItem(ItemId).GetAwaiter().GetResult(); + dynamic item = XivApi.GetItem((int)ItemId).GetAwaiter().GetResult(); ItemName = item.Name; } } @@ -106,7 +106,7 @@ namespace Dalamud.Game.Chat.SeStringHandling.Payloads // unk reader.ReadBytes(3); - var itemNameLen = GetInteger(reader); + var itemNameLen = (int)GetInteger(reader); var itemNameBytes = reader.ReadBytes(itemNameLen); // HQ items have the HQ symbol as part of the name, but since we already recorded diff --git a/Dalamud/Game/Chat/SeStringHandling/Payloads/MapLinkPayload.cs b/Dalamud/Game/Chat/SeStringHandling/Payloads/MapLinkPayload.cs index 74d5b0000..8060333d9 100644 --- a/Dalamud/Game/Chat/SeStringHandling/Payloads/MapLinkPayload.cs +++ b/Dalamud/Game/Chat/SeStringHandling/Payloads/MapLinkPayload.cs @@ -10,8 +10,8 @@ namespace Dalamud.Game.Chat.SeStringHandling.Payloads public override PayloadType Type => PayloadType.MapLink; // pre-Resolve() values - public int TerritoryTypeId { get; set; } - public int MapId { get; set; } + public uint TerritoryTypeId { get; set; } + public uint MapId { get; set; } public uint RawX { get; set; } public uint RawY { get; set; } @@ -29,8 +29,8 @@ namespace Dalamud.Game.Chat.SeStringHandling.Payloads // eventually we should allow creation using 'nice' values that then encode properly var packedTerritoryAndMapBytes = MakePackedInteger(TerritoryTypeId, MapId); - var xBytes = MakeInteger((int)RawX); - var yBytes = MakeInteger((int)RawY); + var xBytes = MakeInteger(RawX); + var yBytes = MakeInteger(RawY); var chunkLen = 4 + packedTerritoryAndMapBytes.Length + xBytes.Length + yBytes.Length; @@ -54,11 +54,11 @@ namespace Dalamud.Game.Chat.SeStringHandling.Payloads { if (string.IsNullOrEmpty(Territory)) { - var terrRow = dataResolver.GetExcelSheet().GetRow(TerritoryTypeId); + var terrRow = dataResolver.GetExcelSheet().GetRow((int)TerritoryTypeId); Territory = dataResolver.GetExcelSheet().GetRow(terrRow.PlaceName).Name; Zone = dataResolver.GetExcelSheet().GetRow(terrRow.PlaceNameZone).Name; - var mapSizeFactor = dataResolver.GetExcelSheet().GetRow(MapId).SizeFactor; + var mapSizeFactor = dataResolver.GetExcelSheet().GetRow((int)MapId).SizeFactor; XCoord = ConvertRawPositionToMapCoordinate(RawX, mapSizeFactor); YCoord = ConvertRawPositionToMapCoordinate(RawY, mapSizeFactor); } diff --git a/Dalamud/Game/Chat/SeStringHandling/Payloads/PlayerPayload.cs b/Dalamud/Game/Chat/SeStringHandling/Payloads/PlayerPayload.cs index 15f555445..16dd6ab45 100644 --- a/Dalamud/Game/Chat/SeStringHandling/Payloads/PlayerPayload.cs +++ b/Dalamud/Game/Chat/SeStringHandling/Payloads/PlayerPayload.cs @@ -12,12 +12,12 @@ namespace Dalamud.Game.Chat.SeStringHandling.Payloads public override PayloadType Type => PayloadType.Player; public string PlayerName { get; private set; } - public int ServerId { get; private set; } + public uint ServerId { get; private set; } public string ServerName { get; private set; } = String.Empty; public PlayerPayload() { } - public PlayerPayload(string playerName, int serverId) + public PlayerPayload(string playerName, uint serverId) { PlayerName = playerName; ServerId = serverId; @@ -78,7 +78,7 @@ namespace Dalamud.Game.Chat.SeStringHandling.Payloads // unk reader.ReadBytes(2); - var nameLen = GetInteger(reader); + var nameLen = (int)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 index 410a044df..e76d1590f 100644 --- a/Dalamud/Game/Chat/SeStringHandling/Payloads/StatusPayload.cs +++ b/Dalamud/Game/Chat/SeStringHandling/Payloads/StatusPayload.cs @@ -11,13 +11,13 @@ namespace Dalamud.Game.Chat.SeStringHandling.Payloads { public override PayloadType Type => PayloadType.Status; - public int StatusId { get; private set; } + public uint StatusId { get; private set; } public string StatusName { get; private set; } = string.Empty; public StatusPayload() { } - public StatusPayload(int statusId) + public StatusPayload(uint statusId) { StatusId = statusId; } diff --git a/Dalamud/Game/ChatHandlers.cs b/Dalamud/Game/ChatHandlers.cs index 4d5861f23..eb7e8573e 100644 --- a/Dalamud/Game/ChatHandlers.cs +++ b/Dalamud/Game/ChatHandlers.cs @@ -187,7 +187,7 @@ namespace Dalamud.Game { if (!valueInfo.Success || !int.TryParse(valueInfo.Value.Replace(",", "").Replace(".", ""), out var itemValue)) continue; - Task.Run(() => this.dalamud.BotManager.ProcessRetainerSale(itemLink.ItemId, itemValue, itemLink.IsHQ)); + Task.Run(() => this.dalamud.BotManager.ProcessRetainerSale((int)itemLink.ItemId, itemValue, itemLink.IsHQ)); break; } } From 15e8844514b0559674027770a7d1aa59bccd7a4c Mon Sep 17 00:00:00 2001 From: meli <57847713+ff-meli@users.noreply.github.com> Date: Thu, 2 Apr 2020 22:20:06 -0700 Subject: [PATCH 4/5] Remove temp lumina injection; needs to happen somehow, but can hopefully be done a bit more cleanly. --- Dalamud/Dalamud.cs | 3 --- Dalamud/Game/Chat/SeStringHandling/Payload.cs | 10 +--------- .../Payloads/MapLinkPayload.cs | 19 ++++++++++--------- .../Game/Chat/SeStringHandling/SeString.cs | 4 +--- 4 files changed, 12 insertions(+), 24 deletions(-) diff --git a/Dalamud/Dalamud.cs b/Dalamud/Dalamud.cs index 5dcc85c8e..3ed3b2ef8 100644 --- a/Dalamud/Dalamud.cs +++ b/Dalamud/Dalamud.cs @@ -88,9 +88,6 @@ namespace Dalamud { this.Data = new DataManager(this.StartInfo.Language); this.Data.Initialize(); - // FIXME: need a better way to get this into the string payloads - Game.Chat.SeStringHandling.SeString.DataResolver = this.Data; - this.ClientState = new ClientState(this, info, this.SigScanner); this.BotManager = new DiscordBotManager(this, this.Configuration.DiscordFeatureConfig); diff --git a/Dalamud/Game/Chat/SeStringHandling/Payload.cs b/Dalamud/Game/Chat/SeStringHandling/Payload.cs index 8f48c4174..b932a338d 100644 --- a/Dalamud/Game/Chat/SeStringHandling/Payload.cs +++ b/Dalamud/Game/Chat/SeStringHandling/Payload.cs @@ -2,7 +2,6 @@ using System; using System.Collections.Generic; using System.IO; using System.Linq; -using Dalamud.Data; using Dalamud.Game.Chat.SeStringHandling.Payloads; using Serilog; @@ -15,15 +14,13 @@ namespace Dalamud.Game.Chat.SeStringHandling { public abstract PayloadType Type { get; } - protected DataManager dataResolver; - public abstract void Resolve(); public abstract byte[] Encode(); protected abstract void ProcessChunkImpl(BinaryReader reader, long endOfStream); - public static Payload Process(BinaryReader reader, DataManager dataResolver) + public static Payload Process(BinaryReader reader) { Payload payload = null; if ((byte)reader.PeekChar() != START_BYTE) @@ -35,11 +32,6 @@ namespace Dalamud.Game.Chat.SeStringHandling payload = ProcessChunk(reader); } - if (payload != null) - { - payload.dataResolver = dataResolver; - } - return payload; } diff --git a/Dalamud/Game/Chat/SeStringHandling/Payloads/MapLinkPayload.cs b/Dalamud/Game/Chat/SeStringHandling/Payloads/MapLinkPayload.cs index 8060333d9..6115adab2 100644 --- a/Dalamud/Game/Chat/SeStringHandling/Payloads/MapLinkPayload.cs +++ b/Dalamud/Game/Chat/SeStringHandling/Payloads/MapLinkPayload.cs @@ -52,16 +52,17 @@ namespace Dalamud.Game.Chat.SeStringHandling.Payloads public override void Resolve() { - if (string.IsNullOrEmpty(Territory)) - { - var terrRow = dataResolver.GetExcelSheet().GetRow((int)TerritoryTypeId); - Territory = dataResolver.GetExcelSheet().GetRow(terrRow.PlaceName).Name; - Zone = dataResolver.GetExcelSheet().GetRow(terrRow.PlaceNameZone).Name; + // TODO: add once lumina DI is figured out + //if (string.IsNullOrEmpty(Territory)) + //{ + // var terrRow = dataResolver.GetExcelSheet().GetRow((int)TerritoryTypeId); + // Territory = dataResolver.GetExcelSheet().GetRow(terrRow.PlaceName).Name; + // Zone = dataResolver.GetExcelSheet().GetRow(terrRow.PlaceNameZone).Name; - var mapSizeFactor = dataResolver.GetExcelSheet().GetRow((int)MapId).SizeFactor; - XCoord = ConvertRawPositionToMapCoordinate(RawX, mapSizeFactor); - YCoord = ConvertRawPositionToMapCoordinate(RawY, mapSizeFactor); - } + // var mapSizeFactor = dataResolver.GetExcelSheet().GetRow((int)MapId).SizeFactor; + // XCoord = ConvertRawPositionToMapCoordinate(RawX, mapSizeFactor); + // YCoord = ConvertRawPositionToMapCoordinate(RawY, mapSizeFactor); + //} } protected override void ProcessChunkImpl(BinaryReader reader, long endOfStream) diff --git a/Dalamud/Game/Chat/SeStringHandling/SeString.cs b/Dalamud/Game/Chat/SeStringHandling/SeString.cs index 0bd9b3b51..87e7dee62 100644 --- a/Dalamud/Game/Chat/SeStringHandling/SeString.cs +++ b/Dalamud/Game/Chat/SeStringHandling/SeString.cs @@ -14,8 +14,6 @@ namespace Dalamud.Game.Chat.SeStringHandling /// public class SeString { - public static DataManager DataResolver { get; set; } - public List Payloads { get; } public SeString(List payloads) @@ -59,7 +57,7 @@ namespace Dalamud.Game.Chat.SeStringHandling while (stream.Position < bytes.Length) { - var payload = Payload.Process(reader, DataResolver); + var payload = Payload.Process(reader); if (payload != null) payloads.Add(payload); } From 0e30309b2d5e7e5f15422bed533bf8ae05a9afc1 Mon Sep 17 00:00:00 2001 From: meli <57847713+ff-meli@users.noreply.github.com> Date: Thu, 2 Apr 2020 22:41:44 -0700 Subject: [PATCH 5/5] add auto-translate/completion payloads; actual resolution logic is known, but waiting on lumina DI in payloads being (re)done --- Dalamud/Game/Chat/SeStringHandling/Payload.cs | 5 + .../Game/Chat/SeStringHandling/PayloadType.cs | 9 +- .../Payloads/AutoTranslatePayload.cs | 105 ++++++++++++++++++ 3 files changed, 114 insertions(+), 5 deletions(-) create mode 100644 Dalamud/Game/Chat/SeStringHandling/Payloads/AutoTranslatePayload.cs diff --git a/Dalamud/Game/Chat/SeStringHandling/Payload.cs b/Dalamud/Game/Chat/SeStringHandling/Payload.cs index b932a338d..25b4531de 100644 --- a/Dalamud/Game/Chat/SeStringHandling/Payload.cs +++ b/Dalamud/Game/Chat/SeStringHandling/Payload.cs @@ -80,6 +80,10 @@ namespace Dalamud.Game.Chat.SeStringHandling } break; + case SeStringChunkType.AutoTranslateKey: + payload = new AutoTranslatePayload(); + break; + case SeStringChunkType.UIForeground: payload = new UIForegroundPayload(); break; @@ -119,6 +123,7 @@ namespace Dalamud.Game.Chat.SeStringHandling protected enum SeStringChunkType { Interactable = 0x27, + AutoTranslateKey = 0x2E, UIForeground = 0x48, UIGlow = 0x49 } diff --git a/Dalamud/Game/Chat/SeStringHandling/PayloadType.cs b/Dalamud/Game/Chat/SeStringHandling/PayloadType.cs index c4e8015a3..92e8afa44 100644 --- a/Dalamud/Game/Chat/SeStringHandling/PayloadType.cs +++ b/Dalamud/Game/Chat/SeStringHandling/PayloadType.cs @@ -1,8 +1,3 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; namespace Dalamud.Game.Chat.SeStringHandling { @@ -40,6 +35,10 @@ namespace Dalamud.Game.Chat.SeStringHandling /// MapLink, /// + /// An SeString payload representing an auto-translate dictionary entry. + /// + AutoTranslateText, + /// /// An SeString payload representing any data we don't handle. /// Unknown diff --git a/Dalamud/Game/Chat/SeStringHandling/Payloads/AutoTranslatePayload.cs b/Dalamud/Game/Chat/SeStringHandling/Payloads/AutoTranslatePayload.cs new file mode 100644 index 000000000..87b25aa45 --- /dev/null +++ b/Dalamud/Game/Chat/SeStringHandling/Payloads/AutoTranslatePayload.cs @@ -0,0 +1,105 @@ +using System; +using System.Collections.Generic; +using System.IO; + + +namespace Dalamud.Game.Chat.SeStringHandling.Payloads +{ + public class AutoTranslatePayload : Payload + { + public override PayloadType Type => PayloadType.AutoTranslateText; + + public uint Group { get; set; } + + public uint Key { get; set; } + + public string Text { get; set; } + + public override void Resolve() + { + // TODO: fixup once lumina DI is in + + //if (string.IsNullOrEmpty(Text)) + //{ + // var sheet = dalamud.Data.GetExcelSheet(); + + // Completion row = null; + // try + // { + // // try to get the row in the Completion table itself, because this is 'easiest' + // // The row may not exist at all (if the Key is for another table), or it could be the wrong row + // // (again, if it's meant for another table) + // row = sheet.GetRow(Key); + // } + // catch {} // don't care, row will be null + + // if (row?.Group == Group) + // { + // // if the row exists in this table and the group matches, this is actually the correct data + // Text = $"{{ {row.Text} }} "; + // } + // else + // { + // Log.Verbose("row mismatch"); + // try + // { + // // we need to get the linked table and do the lookup there instead + // // in this case, there will only be one entry for this group id + // row = sheet.GetRows().First(r => r.Group == Group); + // // many of the names contain valid id ranges after the table name, but we don't need those + // var actualTableName = row.LookupTable.Split('[')[0]; + + // var name = actualTableName switch + // { + // // TODO: rest of xref'd tables + // "Action" => dalamud.Data.GetExcelSheet().GetRow(Key).Name, + // "ClassJob" => dalamud.Data.GetExcelSheet().GetRow(Key).Name, + // "CraftAction" => dalamud.Data.GetExcelSheet().GetRow(Key).Name, + // "Mount" => dalamud.Data.GetExcelSheet().GetRow(Key).Singular, + // "PlaceName" => dalamud.Data.GetExcelSheet().GetRow(Key).Name, + // "Race" => dalamud.Data.GetExcelSheet().GetRow(Key).Masculine, + // _ => throw new Exception(actualTableName) + // }; + + // Text = $"{{ {name} }} "; + // } + // catch (Exception e) + // { + // Log.Error(e, $"AutoTranslatePayload - failed to resolve: {this}"); + // } + // } + //} + } + + public override byte[] Encode() + { + var keyBytes = MakeInteger(Key); + + var chunkLen = keyBytes.Length + 2; + var bytes = new List() + { + START_BYTE, + (byte)SeStringChunkType.AutoTranslateKey, (byte)chunkLen, + (byte)Group + }; + bytes.AddRange(keyBytes); + bytes.Add(END_BYTE); + + return bytes.ToArray(); + } + + public override string ToString() + { + return $"{Type} - Group: {Group}, Key: {Key}, Text: {Text}"; + } + + protected override void ProcessChunkImpl(BinaryReader reader, long endOfStream) + { + // this seems to always be a bare byte, and not following normal integer encoding + // the values in the table are all <70 so this is presumably ok + Group = reader.ReadByte(); + + Key = GetInteger(reader); + } + } +}