Merge branch 'map_links' into sestring_ui_payloads. Some hand-fixup of logic

# Conflicts:
#	Dalamud/Game/Chat/SeStringHandling/Payload.cs
#	Dalamud/Game/Chat/SeStringHandling/PayloadType.cs
#	Dalamud/Game/Chat/SeStringHandling/Payloads/ItemPayload.cs
This commit is contained in:
meli 2020-04-02 21:40:42 -07:00
commit 6d50cdc398
6 changed files with 208 additions and 22 deletions

View file

@ -88,6 +88,9 @@ namespace Dalamud {
this.Data = new DataManager(this.StartInfo.Language); this.Data = new DataManager(this.StartInfo.Language);
this.Data.Initialize(); 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.ClientState = new ClientState(this, info, this.SigScanner);
this.BotManager = new DiscordBotManager(this, this.Configuration.DiscordFeatureConfig); this.BotManager = new DiscordBotManager(this, this.Configuration.DiscordFeatureConfig);

View file

@ -2,6 +2,7 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using Dalamud.Data;
using Dalamud.Game.Chat.SeStringHandling.Payloads; using Dalamud.Game.Chat.SeStringHandling.Payloads;
using Serilog; using Serilog;
@ -14,22 +15,32 @@ namespace Dalamud.Game.Chat.SeStringHandling
{ {
public abstract PayloadType Type { get; } public abstract PayloadType Type { get; }
protected DataManager dataResolver;
public abstract void Resolve(); public abstract void Resolve();
public abstract byte[] Encode(); public abstract byte[] Encode();
protected abstract void ProcessChunkImpl(BinaryReader reader, long endOfStream); 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) if ((byte)reader.PeekChar() != START_BYTE)
{ {
return ProcessText(reader); payload = ProcessText(reader);
} }
else else
{ {
return ProcessChunk(reader); payload = ProcessChunk(reader);
} }
if (payload != null)
{
payload.dataResolver = dataResolver;
}
return payload;
} }
private static Payload ProcessChunk(BinaryReader reader) private static Payload ProcessChunk(BinaryReader reader)
@ -57,6 +68,10 @@ namespace Dalamud.Game.Chat.SeStringHandling
payload = new ItemPayload(); payload = new ItemPayload();
break; break;
case EmbeddedInfoType.MapPositionLink:
payload = new MapLinkPayload();
break;
case EmbeddedInfoType.Status: case EmbeddedInfoType.Status:
payload = new StatusPayload(); payload = new StatusPayload();
break; break;
@ -120,6 +135,7 @@ namespace Dalamud.Game.Chat.SeStringHandling
{ {
PlayerName = 0x01, PlayerName = 0x01,
ItemLink = 0x03, ItemLink = 0x03,
MapPositionLink = 0x04,
Status = 0x09, Status = 0x09,
LinkTerminator = 0xCF // not clear but seems to always follow a link LinkTerminator = 0xCF // not clear but seems to always follow a link
@ -127,13 +143,14 @@ namespace Dalamud.Game.Chat.SeStringHandling
protected enum IntegerType protected enum IntegerType
{ {
// Custom value indicating no marker at all // used as an internal marker; sometimes single bytes are bare with no marker at all
None = 0x0, None = 0,
Byte = 0xF0, Byte = 0xF0,
ByteTimes256 = 0xF1, ByteTimes256 = 0xF1,
Int16 = 0xF2, 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, Int24 = 0xFA,
Int32 = 0xFE Int32 = 0xFE
} }
@ -159,25 +176,22 @@ namespace Dalamud.Game.Chat.SeStringHandling
{ {
case IntegerType.Byte: case IntegerType.Byte:
return input.ReadByte(); return input.ReadByte();
case IntegerType.ByteTimes256: case IntegerType.ByteTimes256:
return input.ReadByte() * 256; return input.ReadByte() * 256;
case IntegerType.Int16: case IntegerType.Int16:
// fallthrough - same logic
case IntegerType.Int16Packed:
{ {
var v = 0; var v = 0;
v |= input.ReadByte() << 8; v |= input.ReadByte() << 8;
v |= input.ReadByte(); v |= input.ReadByte();
return v; return v;
} }
case IntegerType.Int16Plus1Million:
{ case IntegerType.Int24Special:
var v = 0; // Fallthrough - same logic
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: case IntegerType.Int24:
{ {
var v = 0; var v = 0;
@ -186,6 +200,7 @@ namespace Dalamud.Game.Chat.SeStringHandling
v |= input.ReadByte(); v |= input.ReadByte();
return v; return v;
} }
case IntegerType.Int32: case IntegerType.Int32:
{ {
var v = 0; var v = 0;
@ -195,12 +210,13 @@ namespace Dalamud.Game.Chat.SeStringHandling
v |= input.ReadByte(); v |= input.ReadByte();
return v; return v;
} }
default: default:
throw new NotSupportedException(); 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 // single-byte values below the marker values have no marker and have 1 added
if (value + 1 < (int)IntegerType.Byte) if (value + 1 < (int)IntegerType.Byte)
@ -215,10 +231,13 @@ namespace Dalamud.Game.Chat.SeStringHandling
var encodedNum = new List<byte>(); var encodedNum = new List<byte>();
var marker = GetMarkerForIntegerBytes(shrunkValue); if (withMarker)
if (marker != 0)
{ {
encodedNum.Add(marker); var marker = GetMarkerForIntegerBytes(shrunkValue);
if (marker != 0)
{
encodedNum.Add(marker);
}
} }
encodedNum.AddRange(shrunkValue); encodedNum.AddRange(shrunkValue);
@ -244,6 +263,49 @@ namespace Dalamud.Game.Chat.SeStringHandling
return (byte)marker; 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<byte>();
if (withMarker)
{
valueBytes.Add(GetMarkerForPackedIntegerBytes(value));
}
valueBytes.AddRange(value);
return valueBytes.ToArray();
}
#endregion #endregion
} }
} }

View file

@ -36,6 +36,10 @@ namespace Dalamud.Game.Chat.SeStringHandling
/// </summary> /// </summary>
UIGlow, UIGlow,
/// <summary> /// <summary>
/// An SeString payload representing a map position link, such as from &lt;flag&gt; or &lt;pos&gt;.
/// </summary>
MapLink,
/// <summary>
/// An SeString payload representing any data we don't handle. /// An SeString payload representing any data we don't handle.
/// </summary> /// </summary>
Unknown Unknown

View file

@ -125,7 +125,7 @@ namespace Dalamud.Game.Chat.SeStringHandling.Payloads
// custom marker just for hq items? // custom marker just for hq items?
if (bytes.Length == 3 && IsHQ) if (bytes.Length == 3 && IsHQ)
{ {
return (byte)IntegerType.Int16Plus1Million; return (byte)IntegerType.Int24Special;
} }
return base.GetMarkerForIntegerBytes(bytes); return base.GetMarkerForIntegerBytes(bytes);

View file

@ -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<byte>()
{
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<TerritoryType>().GetRow(TerritoryTypeId);
Territory = dataResolver.GetExcelSheet<PlaceName>().GetRow(terrRow.PlaceName).Name;
Zone = dataResolver.GetExcelSheet<PlaceName>().GetRow(terrRow.PlaceNameZone).Name;
var mapSizeFactor = dataResolver.GetExcelSheet<Map>().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;
}
}
}

View file

@ -4,6 +4,7 @@ using System.IO;
using System.Linq; using System.Linq;
using System.Text; using System.Text;
using System.Threading.Tasks; using System.Threading.Tasks;
using Dalamud.Data;
using Dalamud.Game.Chat.SeStringHandling.Payloads; using Dalamud.Game.Chat.SeStringHandling.Payloads;
namespace Dalamud.Game.Chat.SeStringHandling namespace Dalamud.Game.Chat.SeStringHandling
@ -13,6 +14,8 @@ namespace Dalamud.Game.Chat.SeStringHandling
/// </summary> /// </summary>
public class SeString public class SeString
{ {
public static DataManager DataResolver { get; set; }
public List<Payload> Payloads { get; } public List<Payload> Payloads { get; }
public SeString(List<Payload> payloads) public SeString(List<Payload> payloads)
@ -56,7 +59,7 @@ namespace Dalamud.Game.Chat.SeStringHandling
while (stream.Position < bytes.Length) while (stream.Position < bytes.Length)
{ {
var payload = Payload.Process(reader); var payload = Payload.Process(reader, DataResolver);
if (payload != null) if (payload != null)
payloads.Add(payload); payloads.Add(payload);
} }