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 685b4c8a5..665e6a73d 100644 --- a/Dalamud/Game/Chat/SeStringHandling/Payload.cs +++ b/Dalamud/Game/Chat/SeStringHandling/Payload.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; using System.IO; using System.Linq; +using Dalamud.Data; using Dalamud.Game.Chat.SeStringHandling.Payloads; using Serilog; @@ -14,22 +15,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) @@ -57,6 +68,10 @@ namespace Dalamud.Game.Chat.SeStringHandling payload = new ItemPayload(); break; + case EmbeddedInfoType.MapPositionLink: + payload = new MapLinkPayload(); + break; + case EmbeddedInfoType.Status: payload = new StatusPayload(); break; @@ -120,6 +135,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 @@ -127,13 +143,14 @@ namespace Dalamud.Game.Chat.SeStringHandling protected enum IntegerType { - // Custom value indicating no marker at all - None = 0x0, + // 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 } @@ -159,25 +176,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; @@ -186,6 +200,7 @@ namespace Dalamud.Game.Chat.SeStringHandling v |= input.ReadByte(); return v; } + case IntegerType.Int32: { var v = 0; @@ -195,12 +210,13 @@ namespace Dalamud.Game.Chat.SeStringHandling v |= input.ReadByte(); return v; } + default: throw new NotSupportedException(); } } - protected virtual byte[] MakeInteger(int value) + protected virtual byte[] MakeInteger(int value, bool withMarker = true) { // single-byte values below the marker values have no marker and have 1 added if (value + 1 < (int)IntegerType.Byte) @@ -215,10 +231,13 @@ namespace Dalamud.Game.Chat.SeStringHandling var encodedNum = new List(); - var marker = GetMarkerForIntegerBytes(shrunkValue); - if (marker != 0) + if (withMarker) { - encodedNum.Add(marker); + var marker = GetMarkerForIntegerBytes(shrunkValue); + if (marker != 0) + { + encodedNum.Add(marker); + } } encodedNum.AddRange(shrunkValue); @@ -244,6 +263,49 @@ namespace Dalamud.Game.Chat.SeStringHandling return (byte)marker; } + + protected virtual byte GetMarkerForPackedIntegerBytes(byte[] bytes) + { + // So far I've only ever seen this with 2 8-bit values packed into a short + var type = bytes.Length switch + { + 2 => IntegerType.Int16Packed, + _ => throw new NotSupportedException() + }; + + return (byte)type; + } + + protected (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 byte[] MakePackedInteger(int val1, int val2, bool withMarker = true) + { + var value = MakeInteger(val1, false).Concat(MakeInteger(val2, false)).ToArray(); + + var valueBytes = new List(); + if (withMarker) + { + valueBytes.Add(GetMarkerForPackedIntegerBytes(value)); + } + + valueBytes.AddRange(value); + + return valueBytes.ToArray(); + } #endregion } } diff --git a/Dalamud/Game/Chat/SeStringHandling/PayloadType.cs b/Dalamud/Game/Chat/SeStringHandling/PayloadType.cs index 2a6107a76..c4e8015a3 100644 --- a/Dalamud/Game/Chat/SeStringHandling/PayloadType.cs +++ b/Dalamud/Game/Chat/SeStringHandling/PayloadType.cs @@ -36,6 +36,10 @@ namespace Dalamud.Game.Chat.SeStringHandling /// UIGlow, /// + /// 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 7ebf6ba23..317214910 100644 --- a/Dalamud/Game/Chat/SeStringHandling/Payloads/ItemPayload.cs +++ b/Dalamud/Game/Chat/SeStringHandling/Payloads/ItemPayload.cs @@ -125,7 +125,7 @@ namespace Dalamud.Game.Chat.SeStringHandling.Payloads // custom marker just for hq items? if (bytes.Length == 3 && IsHQ) { - return (byte)IntegerType.Int16Plus1Million; + return (byte)IntegerType.Int24Special; } return base.GetMarkerForIntegerBytes(bytes); diff --git a/Dalamud/Game/Chat/SeStringHandling/Payloads/MapLinkPayload.cs b/Dalamud/Game/Chat/SeStringHandling/Payloads/MapLinkPayload.cs new file mode 100644 index 000000000..74d5b0000 --- /dev/null +++ b/Dalamud/Game/Chat/SeStringHandling/Payloads/MapLinkPayload.cs @@ -0,0 +1,114 @@ +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; } + // there is no Z; it's purely in the text payload where applicable + + 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 = MakePackedInteger(TerritoryTypeId, MapId); + var xBytes = MakeInteger((int)RawX); + var yBytes = MakeInteger((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 + + protected override byte GetMarkerForIntegerBytes(byte[] bytes) + { + var type = bytes.Length switch + { + 3 => (byte)IntegerType.Int24Special, // used because seen in incoming data + 2 => (byte)IntegerType.Int16, + 1 => (byte)IntegerType.None, // single bytes seem to have no prefix at all here + _ => base.GetMarkerForIntegerBytes(bytes) + }; + + return 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); }