This commit is contained in:
goat 2020-05-11 19:35:58 +02:00
commit de85271b73
23 changed files with 1646 additions and 384 deletions

View file

@ -12,6 +12,7 @@ using Dalamud.Data;
using Dalamud.DiscordBot;
using Dalamud.Game;
using Dalamud.Game.Chat;
using Dalamud.Game.Chat.SeStringHandling;
using Dalamud.Game.ClientState;
using Dalamud.Game.ClientState.Actors.Types;
using Dalamud.Game.ClientState.Actors.Types.NonPlayer;
@ -116,6 +117,9 @@ namespace Dalamud {
this.Data = new DataManager(this.StartInfo.Language);
await this.Data.Initialize(this.baseDirectory);
// TODO: better way to do this? basically for lumina injection
SeString.Dalamud = this;
this.NetworkHandlers = new NetworkHandlers(this, this.Configuration.OptOutMbCollection);
// Initialize managers. Basically handlers for the logic
@ -123,7 +127,6 @@ namespace Dalamud {
SetupCommands();
this.ChatHandlers = new ChatHandlers(this);
// Discord Bot Manager
this.BotManager = new DiscordBotManager(this, this.Configuration.DiscordFeatureConfig);
this.BotManager.Start();
@ -558,25 +561,14 @@ namespace Dalamud {
private ItemSearchWindow itemSearchCommandWindow;
private bool isImguiDrawItemSearchWindow;
private void OnItemLinkCommand(string command, string arguments) {
private void OnItemLinkCommand(string command, string arguments)
{
this.itemSearchCommandWindow = new ItemSearchWindow(this.Data, new UiBuilder(this.InterfaceManager, "ItemSearcher"), false, arguments);
this.itemSearchCommandWindow.OnItemChosen += (sender, item) => {
var hexData = new byte[] {
0x02, 0x13, 0x06, 0xFE, 0xFF, 0xF3, 0xF3, 0xF3, 0x03, 0x02, 0x27, 0x07, 0x03, 0xF2, 0x3A, 0x2F,
0x02, 0x01, 0x03, 0x02, 0x13, 0x06, 0xFE, 0xFF, 0xFF, 0x7B, 0x1A, 0x03, 0xEE, 0x82, 0xBB, 0x02,
0x13, 0x02, 0xEC, 0x03
};
var endTag = new byte[] {
0x02, 0x27, 0x07, 0xCF, 0x01, 0x01, 0x01, 0xFF, 0x01, 0x03, 0x02, 0x13, 0x02, 0xEC, 0x03
};
BitConverter.GetBytes((short) item.RowId).Reverse().ToArray().CopyTo(hexData, 14);
hexData = hexData.Concat(Encoding.UTF8.GetBytes(item.Name)).Concat(endTag).ToArray();
this.Framework.Gui.Chat.PrintChat(new XivChatEntry {
MessageBytes = hexData
this.itemSearchCommandWindow.OnItemChosen += (sender, item) =>
{
this.Framework.Gui.Chat.PrintChat(new XivChatEntry
{
MessageBytes = SeStringUtils.CreateItemLink(item, false).Encode()
});
};
this.isImguiDrawItemSearchWindow = true;

View file

@ -0,0 +1,53 @@
using Lumina.Excel;
namespace Dalamud.Data.TransientSheet
{
[Sheet( "Completion", columnHash: 0x2e6c55a3 )]
public class Completion : IExcelRow
{
// column defs from Mon, 02 Mar 2020 11:00:20 GMT
// col: 03 offset: 0000
public string Text;
// col: 04 offset: 0004
public string GroupTitle;
// col: 02 offset: 0008
public string LookupTable;
// col: 00 offset: 000c
public ushort Group;
// col: 01 offset: 000e
public ushort Key;
public int RowId { get; set; }
public int SubRowId { get; set; }
public void PopulateData( RowParser parser, Lumina.Lumina lumina )
{
RowId = parser.Row;
SubRowId = parser.SubRow;
// col: 3 offset: 0000
Text = parser.ReadOffset< string >( 0x0 );
// col: 4 offset: 0004
GroupTitle = parser.ReadOffset< string >( 0x4 );
// col: 2 offset: 0008
LookupTable = parser.ReadOffset< string >( 0x8 );
// col: 0 offset: 000c
Group = parser.ReadOffset< ushort >( 0xc );
// col: 1 offset: 000e
Key = parser.ReadOffset< ushort >( 0xe );
}
}
}

View file

@ -0,0 +1,401 @@
using Lumina.Excel;
namespace Dalamud.Data.TransientSheet
{
[Sheet( "PetMirage", columnHash: 0x720608f1 )]
public class PetMirage : IExcelRow
{
// column defs from Sun, 26 Apr 2020 15:17:06 GMT
// col: 02 offset: 0000
public string Name;
// col: 03 offset: 0004
public ushort unknown4;
// col: 33 offset: 0006
public ushort unknown6;
// col: 48 offset: 0008
public ushort unknown8;
// col: 18 offset: 000a
public byte unknowna;
// col: 04 offset: 000c
public ushort unknownc;
// col: 34 offset: 000e
public ushort unknowne;
// col: 49 offset: 0010
public ushort unknown10;
// col: 19 offset: 0012
public byte unknown12;
// col: 05 offset: 0014
public ushort unknown14;
// col: 35 offset: 0016
public ushort unknown16;
// col: 50 offset: 0018
public ushort unknown18;
// col: 20 offset: 001a
public byte unknown1a;
// col: 06 offset: 001c
public ushort unknown1c;
// col: 36 offset: 001e
public ushort unknown1e;
// col: 51 offset: 0020
public ushort unknown20;
// col: 21 offset: 0022
public byte unknown22;
// col: 07 offset: 0024
public ushort unknown24;
// col: 37 offset: 0026
public ushort unknown26;
// col: 52 offset: 0028
public ushort unknown28;
// col: 22 offset: 002a
public byte unknown2a;
// col: 08 offset: 002c
public ushort unknown2c;
// col: 38 offset: 002e
public ushort unknown2e;
// col: 53 offset: 0030
public ushort unknown30;
// col: 23 offset: 0032
public byte unknown32;
// col: 09 offset: 0034
public ushort unknown34;
// col: 39 offset: 0036
public ushort unknown36;
// col: 54 offset: 0038
public ushort unknown38;
// col: 24 offset: 003a
public byte unknown3a;
// col: 10 offset: 003c
public ushort unknown3c;
// col: 40 offset: 003e
public ushort unknown3e;
// col: 55 offset: 0040
public ushort unknown40;
// col: 25 offset: 0042
public byte unknown42;
// col: 11 offset: 0044
public ushort unknown44;
// col: 41 offset: 0046
public ushort unknown46;
// col: 56 offset: 0048
public ushort unknown48;
// col: 26 offset: 004a
public byte unknown4a;
// col: 12 offset: 004c
public ushort unknown4c;
// col: 42 offset: 004e
public ushort unknown4e;
// col: 57 offset: 0050
public ushort unknown50;
// col: 27 offset: 0052
public byte unknown52;
// col: 13 offset: 0054
public ushort unknown54;
// col: 43 offset: 0056
public ushort unknown56;
// col: 58 offset: 0058
public ushort unknown58;
// col: 28 offset: 005a
public byte unknown5a;
// col: 14 offset: 005c
public ushort unknown5c;
// col: 44 offset: 005e
public ushort unknown5e;
// col: 59 offset: 0060
public ushort unknown60;
// col: 29 offset: 0062
public byte unknown62;
// col: 15 offset: 0064
public ushort unknown64;
// col: 45 offset: 0066
public ushort unknown66;
// col: 60 offset: 0068
public ushort unknown68;
// col: 30 offset: 006a
public byte unknown6a;
// col: 16 offset: 006c
public ushort unknown6c;
// col: 46 offset: 006e
public ushort unknown6e;
// col: 61 offset: 0070
public ushort unknown70;
// col: 31 offset: 0072
public byte unknown72;
// col: 17 offset: 0074
public ushort unknown74;
// col: 47 offset: 0076
public ushort unknown76;
// col: 62 offset: 0078
public ushort unknown78;
// col: 32 offset: 007a
public byte unknown7a;
// col: 00 offset: 007c
public float unknown7c;
// col: 01 offset: 0080
public ushort unknown80;
public int RowId { get; set; }
public int SubRowId { get; set; }
public void PopulateData( RowParser parser, Lumina.Lumina lumina )
{
RowId = parser.Row;
SubRowId = parser.SubRow;
// col: 2 offset: 0000
Name = parser.ReadOffset< string >( 0x0 );
// col: 3 offset: 0004
unknown4 = parser.ReadOffset< ushort >( 0x4 );
// col: 33 offset: 0006
unknown6 = parser.ReadOffset< ushort >( 0x6 );
// col: 48 offset: 0008
unknown8 = parser.ReadOffset< ushort >( 0x8 );
// col: 18 offset: 000a
unknowna = parser.ReadOffset< byte >( 0xa );
// col: 4 offset: 000c
unknownc = parser.ReadOffset< ushort >( 0xc );
// col: 34 offset: 000e
unknowne = parser.ReadOffset< ushort >( 0xe );
// col: 49 offset: 0010
unknown10 = parser.ReadOffset< ushort >( 0x10 );
// col: 19 offset: 0012
unknown12 = parser.ReadOffset< byte >( 0x12 );
// col: 5 offset: 0014
unknown14 = parser.ReadOffset< ushort >( 0x14 );
// col: 35 offset: 0016
unknown16 = parser.ReadOffset< ushort >( 0x16 );
// col: 50 offset: 0018
unknown18 = parser.ReadOffset< ushort >( 0x18 );
// col: 20 offset: 001a
unknown1a = parser.ReadOffset< byte >( 0x1a );
// col: 6 offset: 001c
unknown1c = parser.ReadOffset< ushort >( 0x1c );
// col: 36 offset: 001e
unknown1e = parser.ReadOffset< ushort >( 0x1e );
// col: 51 offset: 0020
unknown20 = parser.ReadOffset< ushort >( 0x20 );
// col: 21 offset: 0022
unknown22 = parser.ReadOffset< byte >( 0x22 );
// col: 7 offset: 0024
unknown24 = parser.ReadOffset< ushort >( 0x24 );
// col: 37 offset: 0026
unknown26 = parser.ReadOffset< ushort >( 0x26 );
// col: 52 offset: 0028
unknown28 = parser.ReadOffset< ushort >( 0x28 );
// col: 22 offset: 002a
unknown2a = parser.ReadOffset< byte >( 0x2a );
// col: 8 offset: 002c
unknown2c = parser.ReadOffset< ushort >( 0x2c );
// col: 38 offset: 002e
unknown2e = parser.ReadOffset< ushort >( 0x2e );
// col: 53 offset: 0030
unknown30 = parser.ReadOffset< ushort >( 0x30 );
// col: 23 offset: 0032
unknown32 = parser.ReadOffset< byte >( 0x32 );
// col: 9 offset: 0034
unknown34 = parser.ReadOffset< ushort >( 0x34 );
// col: 39 offset: 0036
unknown36 = parser.ReadOffset< ushort >( 0x36 );
// col: 54 offset: 0038
unknown38 = parser.ReadOffset< ushort >( 0x38 );
// col: 24 offset: 003a
unknown3a = parser.ReadOffset< byte >( 0x3a );
// col: 10 offset: 003c
unknown3c = parser.ReadOffset< ushort >( 0x3c );
// col: 40 offset: 003e
unknown3e = parser.ReadOffset< ushort >( 0x3e );
// col: 55 offset: 0040
unknown40 = parser.ReadOffset< ushort >( 0x40 );
// col: 25 offset: 0042
unknown42 = parser.ReadOffset< byte >( 0x42 );
// col: 11 offset: 0044
unknown44 = parser.ReadOffset< ushort >( 0x44 );
// col: 41 offset: 0046
unknown46 = parser.ReadOffset< ushort >( 0x46 );
// col: 56 offset: 0048
unknown48 = parser.ReadOffset< ushort >( 0x48 );
// col: 26 offset: 004a
unknown4a = parser.ReadOffset< byte >( 0x4a );
// col: 12 offset: 004c
unknown4c = parser.ReadOffset< ushort >( 0x4c );
// col: 42 offset: 004e
unknown4e = parser.ReadOffset< ushort >( 0x4e );
// col: 57 offset: 0050
unknown50 = parser.ReadOffset< ushort >( 0x50 );
// col: 27 offset: 0052
unknown52 = parser.ReadOffset< byte >( 0x52 );
// col: 13 offset: 0054
unknown54 = parser.ReadOffset< ushort >( 0x54 );
// col: 43 offset: 0056
unknown56 = parser.ReadOffset< ushort >( 0x56 );
// col: 58 offset: 0058
unknown58 = parser.ReadOffset< ushort >( 0x58 );
// col: 28 offset: 005a
unknown5a = parser.ReadOffset< byte >( 0x5a );
// col: 14 offset: 005c
unknown5c = parser.ReadOffset< ushort >( 0x5c );
// col: 44 offset: 005e
unknown5e = parser.ReadOffset< ushort >( 0x5e );
// col: 59 offset: 0060
unknown60 = parser.ReadOffset< ushort >( 0x60 );
// col: 29 offset: 0062
unknown62 = parser.ReadOffset< byte >( 0x62 );
// col: 15 offset: 0064
unknown64 = parser.ReadOffset< ushort >( 0x64 );
// col: 45 offset: 0066
unknown66 = parser.ReadOffset< ushort >( 0x66 );
// col: 60 offset: 0068
unknown68 = parser.ReadOffset< ushort >( 0x68 );
// col: 30 offset: 006a
unknown6a = parser.ReadOffset< byte >( 0x6a );
// col: 16 offset: 006c
unknown6c = parser.ReadOffset< ushort >( 0x6c );
// col: 46 offset: 006e
unknown6e = parser.ReadOffset< ushort >( 0x6e );
// col: 61 offset: 0070
unknown70 = parser.ReadOffset< ushort >( 0x70 );
// col: 31 offset: 0072
unknown72 = parser.ReadOffset< byte >( 0x72 );
// col: 17 offset: 0074
unknown74 = parser.ReadOffset< ushort >( 0x74 );
// col: 47 offset: 0076
unknown76 = parser.ReadOffset< ushort >( 0x76 );
// col: 62 offset: 0078
unknown78 = parser.ReadOffset< ushort >( 0x78 );
// col: 32 offset: 007a
unknown7a = parser.ReadOffset< byte >( 0x7a );
// col: 0 offset: 007c
unknown7c = parser.ReadOffset< float >( 0x7c );
// col: 1 offset: 0080
unknown80 = parser.ReadOffset< ushort >( 0x80 );
}
}
}

View file

@ -205,10 +205,8 @@ namespace Dalamud.DiscordBot {
senderWorld = this.dalamud.ClientState.LocalPlayer.HomeWorld.GameData.Name;
} else {
playerLink.Resolve();
senderName = wasOutgoingTell ? this.dalamud.ClientState.LocalPlayer.Name : playerLink.PlayerName;
senderWorld = playerLink.ServerName;
senderWorld = playerLink.World.Name;
}
var rawMessage = SeString.Parse(message.RawData).TextValue;

View file

@ -0,0 +1,9 @@
using System;
namespace Dalamud.Game.Chat.SeStringHandling
{
interface ITextProvider
{
string Text { get; }
}
}

View file

@ -2,15 +2,17 @@ using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using Dalamud.Data;
using Dalamud.Game.Chat.SeStringHandling.Payloads;
using Serilog;
// TODOs:
// - refactor integer handling now that we have multiple packed types
// - common construction/property design for subclasses
// - lumina DI
// - design for handling raw values vs resolved values, both for input and output
// - wrapper class(es) for handling of composite links in chat (item, map etc) and formatting operations
// Maybes:
// - convert parsing to custom structs for each payload? would make some code prettier and easier to work with
// but also wouldn't work out as well for things that are dynamically-sized
// - [SeString] some way to add surrounding formatting information as flags/data to text (or other?) payloads?
// eg, if a text payload is surrounded by italics payloads, strip them out and mark the text payload as italicized
namespace Dalamud.Game.Chat.SeStringHandling
{
@ -19,33 +21,107 @@ namespace Dalamud.Game.Chat.SeStringHandling
/// </summary>
public abstract class Payload
{
/// <summary>
/// The type of this payload.
/// </summary>
public abstract PayloadType Type { get; }
public abstract void Resolve();
/// <summary>
/// Whether this payload has been modified since the last Encode().
/// </summary>
public bool Dirty { get; protected set; } = true;
public abstract byte[] Encode();
/// <summary>
/// Encodes the internal state of this payload into a byte[] suitable for sending to in-game
/// handlers such as the chat log.
/// </summary>
/// <returns>Encoded binary payload data suitable for use with in-game handlers.</returns>
protected abstract byte[] EncodeImpl();
protected abstract void ProcessChunkImpl(BinaryReader reader, long endOfStream);
// TODO: endOfStream is somewhat legacy now that payload length is always handled correctly.
// This could be changed to just take a straight byte[], but that would complicate reading
// but we could probably at least remove the end param
/// <summary>
/// Decodes a byte stream from the game into a payload object.
/// </summary>
/// <param name="reader">A BinaryReader containing at least all the data for this payload.</param>
/// <param name="endOfStream">The location holding the end of the data for this payload.</param>
protected abstract void DecodeImpl(BinaryReader reader, long endOfStream);
public static Payload Process(BinaryReader reader)
/// <summary>
/// The Lumina instance to use for any necessary data lookups.
/// </summary>
protected DataManager dataResolver;
// private for now, since subclasses shouldn't interact with this
// To force-invalidate it, Dirty can be set to true
private byte[] encodedData;
protected Payload()
{
// this is not a good way to do this, but I don't want to have to include a dalamud
// reference on multiple methods in every payload class
// We could also just directly reference this static where we use it, but this at least
// allows for more easily changing how this is injected later, without affecting code
// that makes use of it
this.dataResolver = SeString.Dalamud.Data;
}
/// <summary>
/// Encode this payload object into a byte[] useable in-game for things like the chat log.
/// </summary>
/// <param name="force">If true, ignores any cached value and forcibly reencodes the payload from its internal representation.</param>
/// <returns>A byte[] suitable for use with in-game handlers such as the chat log.</returns>
public byte[] Encode(bool force = false)
{
if (Dirty || force)
{
this.encodedData = EncodeImpl();
Dirty = false;
}
return this.encodedData;
}
/// <summary>
/// Decodes a binary representation of a payload into its corresponding nice object payload.
/// </summary>
/// <param name="reader">A reader positioned at the start of the payload, and containing at least one entire payload.</param>
/// <returns>The constructed Payload-derived object that was decoded from the binary data.</returns>
public static Payload Decode(BinaryReader reader)
{
var payloadStartPos = reader.BaseStream.Position;
Payload payload = null;
var initialByte = reader.ReadByte();
reader.BaseStream.Position--;
if (initialByte != START_BYTE)
{
payload = ProcessText(reader);
payload = DecodeText(reader);
}
else
{
payload = ProcessChunk(reader);
payload = DecodeChunk(reader);
}
// for now, cache off the actual binary data for this payload, so we don't have to
// regenerate it if the payload isn't modified
// TODO: probably better ways to handle this
var payloadEndPos = reader.BaseStream.Position;
reader.BaseStream.Position = payloadStartPos;
payload.encodedData = reader.ReadBytes((int)(payloadEndPos - payloadStartPos));
payload.Dirty = false;
// Log.Verbose($"got payload bytes {BitConverter.ToString(payload.encodedData).Replace("-", " ")}");
reader.BaseStream.Position = payloadEndPos;
return payload;
}
private static Payload ProcessChunk(BinaryReader reader)
private static Payload DecodeChunk(BinaryReader reader)
{
Payload payload = null;
@ -55,8 +131,13 @@ namespace Dalamud.Game.Chat.SeStringHandling
var packetStart = reader.BaseStream.Position;
// any unhandled payload types will be turned into a RawPayload with the exact same binary data
switch (chunkType)
{
case SeStringChunkType.EmphasisItalic:
payload = new EmphasisItalicPayload();
break;
case SeStringChunkType.Interactable:
{
var subType = (EmbeddedInfoType)reader.ReadByte();
@ -81,10 +162,13 @@ namespace Dalamud.Game.Chat.SeStringHandling
case EmbeddedInfoType.LinkTerminator:
// this has no custom handling and so needs to fallthrough to ensure it is captured
default:
Log.Verbose("Unhandled EmbeddedInfoType: {0}", subType);
// but I'm also tired of this log
if (subType != EmbeddedInfoType.LinkTerminator)
{
Log.Verbose("Unhandled EmbeddedInfoType: {0}", subType);
}
// rewind so we capture the Interactable byte in the raw data
reader.BaseStream.Seek(-1, SeekOrigin.Current);
payload = new RawPayload((byte)chunkType);
break;
}
}
@ -104,11 +188,11 @@ namespace Dalamud.Game.Chat.SeStringHandling
default:
Log.Verbose("Unhandled SeStringChunkType: {0}", chunkType);
payload = new RawPayload((byte)chunkType);
break;
}
payload?.ProcessChunkImpl(reader, reader.BaseStream.Position + chunkLen - 1);
payload ??= new RawPayload((byte)chunkType);
payload.DecodeImpl(reader, reader.BaseStream.Position + chunkLen - 1);
// read through the rest of the packet
var readBytes = (uint)(reader.BaseStream.Position - packetStart);
@ -117,10 +201,10 @@ namespace Dalamud.Game.Chat.SeStringHandling
return payload;
}
private static Payload ProcessText(BinaryReader reader)
private static Payload DecodeText(BinaryReader reader)
{
var payload = new TextPayload();
payload.ProcessChunkImpl(reader, reader.BaseStream.Length);
payload.DecodeImpl(reader, reader.BaseStream.Length);
return payload;
}
@ -132,6 +216,7 @@ namespace Dalamud.Game.Chat.SeStringHandling
protected enum SeStringChunkType
{
EmphasisItalic = 0x1A,
Interactable = 0x27,
AutoTranslateKey = 0x2E,
UIForeground = 0x48,
@ -148,6 +233,10 @@ namespace Dalamud.Game.Chat.SeStringHandling
LinkTerminator = 0xCF // not clear but seems to always follow a link
}
// TODO - everything below needs to be completely refactored, now that we have run into
// a lot more cases than were originally handled.
protected enum IntegerType
{
// used as an internal marker; sometimes single bytes are bare with no marker at all
@ -158,8 +247,8 @@ namespace Dalamud.Game.Chat.SeStringHandling
Int16 = 0xF2,
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
Int24Packed = 0xFC, // used in map links- sometimes short+byte, sometimes... not??
Int24 = 0xFA,
Int24Packed = 0xFC, // used in map links- sometimes short+byte, sometimes... not??
Int32 = 0xFE
}

View file

@ -39,6 +39,10 @@ namespace Dalamud.Game.Chat.SeStringHandling
/// </summary>
AutoTranslateText,
/// <summary>
/// An SeString payload representing italic emphasis formatting on text.
/// </summary>
EmphasisItalic,
/// <summary>
/// An SeString payload representing any data we don't handle.
/// </summary>
Unknown

View file

@ -1,86 +1,75 @@
using Dalamud.Data.TransientSheet;
using Lumina.Excel.GeneratedSheets;
using Serilog;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
namespace Dalamud.Game.Chat.SeStringHandling.Payloads
{
public class AutoTranslatePayload : Payload
/// <summary>
/// An SeString Payload containing an auto-translation/completion chat message.
/// </summary>
public class AutoTranslatePayload : Payload, ITextProvider
{
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()
private string text;
/// <summary>
/// The actual text displayed in-game for this payload.
/// </summary>
/// <remarks>
/// Value is evaluated lazily and cached.
/// </remarks>
public string Text
{
// TODO: fixup once lumina DI is in
//if (string.IsNullOrEmpty(Text))
//{
// var sheet = dalamud.Data.GetExcelSheet<Completion>();
// 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<Data.TransientSheet.Action>().GetRow(Key).Name,
// "ClassJob" => dalamud.Data.GetExcelSheet<ClassJob>().GetRow(Key).Name,
// "CraftAction" => dalamud.Data.GetExcelSheet<CraftAction>().GetRow(Key).Name,
// "Mount" => dalamud.Data.GetExcelSheet<Mount>().GetRow(Key).Singular,
// "PlaceName" => dalamud.Data.GetExcelSheet<PlaceName>().GetRow(Key).Name,
// "Race" => dalamud.Data.GetExcelSheet<Race>().GetRow(Key).Masculine,
// _ => throw new Exception(actualTableName)
// };
// Text = $"{{ {name} }} ";
// }
// catch (Exception e)
// {
// Log.Error(e, $"AutoTranslatePayload - failed to resolve: {this}");
// }
// }
//}
get
{
// wrap the text in the colored brackets that is uses in-game, since those
// are not actually part of any of the payloads
this.text ??= $"{(char)SeIconChar.AutoTranslateOpen} {Resolve()} {(char)SeIconChar.AutoTranslateClose}";
return this.text;
}
}
public override byte[] Encode()
private uint group;
private uint key;
internal AutoTranslatePayload() { }
/// <summary>
/// Creates a new auto-translate payload.
/// </summary>
/// <param name="group">The group id for this message.</param>
/// <param name="key">The key/row id for this message. Which table this is in depends on the group id and details the Completion table.</param>
/// <remarks>
/// This table is somewhat complicated in structure, and so using this constructor may not be very nice.
/// There is probably little use to create one of these, however.
/// </remarks>
public AutoTranslatePayload(uint group, uint key)
{
var keyBytes = MakeInteger(Key);
this.group = group;
this.key = key;
}
// TODO: friendlier ctor? not sure how to handle that given how weird the tables are
public override string ToString()
{
return $"{Type} - Group: {group}, Key: {key}, Text: {Text}";
}
protected override byte[] EncodeImpl()
{
var keyBytes = MakeInteger(this.key);
var chunkLen = keyBytes.Length + 2;
var bytes = new List<byte>()
{
START_BYTE,
(byte)SeStringChunkType.AutoTranslateKey, (byte)chunkLen,
(byte)Group
(byte)this.group
};
bytes.AddRange(keyBytes);
bytes.Add(END_BYTE);
@ -88,18 +77,80 @@ namespace Dalamud.Game.Chat.SeStringHandling.Payloads
return bytes.ToArray();
}
public override string ToString()
{
return $"{Type} - Group: {Group}, Key: {Key}, Text: {Text}";
}
protected override void ProcessChunkImpl(BinaryReader reader, long endOfStream)
protected override void DecodeImpl(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();
this.group = reader.ReadByte();
Key = GetInteger(reader);
this.key = GetInteger(reader);
}
private string Resolve()
{
string value = null;
var sheet = this.dataResolver.GetExcelSheet<Completion>();
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((int)this.key);
}
catch { } // don't care, row will be null
if (row?.Group == this.group)
{
// if the row exists in this table and the group matches, this is actually the correct data
value = row.Text;
}
else
{
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 == this.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 ikey = (int)this.key;
var name = actualTableName switch
{
"Action" => this.dataResolver.GetExcelSheet<Lumina.Excel.GeneratedSheets.Action>().GetRow(ikey).Name,
"ActionComboRoute" => this.dataResolver.GetExcelSheet<ActionComboRoute>().GetRow(ikey).Name,
"BuddyAction" => this.dataResolver.GetExcelSheet<BuddyAction>().GetRow(ikey).Name,
"ClassJob" => this.dataResolver.GetExcelSheet<ClassJob>().GetRow(ikey).Name,
"Companion" => this.dataResolver.GetExcelSheet<Companion>().GetRow(ikey).Singular,
"CraftAction" => this.dataResolver.GetExcelSheet<CraftAction>().GetRow(ikey).Name,
"GeneralAction" => this.dataResolver.GetExcelSheet<GeneralAction>().GetRow(ikey).Name,
"GuardianDeity" => this.dataResolver.GetExcelSheet<GuardianDeity>().GetRow(ikey).Name,
"MainCommand" => this.dataResolver.GetExcelSheet<MainCommand>().GetRow(ikey).Name,
"Mount" => this.dataResolver.GetExcelSheet<Mount>().GetRow(ikey).Singular,
"Pet" => this.dataResolver.GetExcelSheet<Pet>().GetRow(ikey).Name,
"PetAction" => this.dataResolver.GetExcelSheet<PetAction>().GetRow(ikey).Name,
"PetMirage" => this.dataResolver.GetExcelSheet<PetMirage>().GetRow(ikey).Name,
"PlaceName" => this.dataResolver.GetExcelSheet<PlaceName>().GetRow(ikey).Name,
"Race" => this.dataResolver.GetExcelSheet<Race>().GetRow(ikey).Masculine,
"TextCommand" => this.dataResolver.GetExcelSheet<TextCommand>().GetRow(ikey).Command,
"Tribe" => this.dataResolver.GetExcelSheet<Tribe>().GetRow(ikey).Masculine,
"Weather" => this.dataResolver.GetExcelSheet<Weather>().GetRow(ikey).Name,
_ => throw new Exception(actualTableName)
};
value = name;
}
catch (Exception e)
{
Log.Error(e, $"AutoTranslatePayload - failed to resolve: {this}");
}
}
return value;
}
}
}

View file

@ -0,0 +1,70 @@
using System;
using System.Collections.Generic;
using System.IO;
namespace Dalamud.Game.Chat.SeStringHandling.Payloads
{
/// <summary>
/// An SeString Payload containing information about enabling or disabling italics formatting on following text.
/// </summary>
/// <remarks>
/// As with other formatting payloads, this is only useful in a payload block, where it affects any subsequent
/// text payloads.
/// </remarks>
public class EmphasisItalicPayload : Payload
{
/// <summary>
/// Payload representing enabling italics on following text.
/// </summary>
public static EmphasisItalicPayload ItalicsOn => new EmphasisItalicPayload(true);
/// <summary>
/// Payload representing disabling italics on following text.
/// </summary>
public static EmphasisItalicPayload ItalicsOff => new EmphasisItalicPayload(false);
public override PayloadType Type => PayloadType.EmphasisItalic;
/// <summary>
/// Whether this payload enables italics formatting for following text.
/// </summary>
public bool IsEnabled { get; private set; }
internal EmphasisItalicPayload() { }
/// <summary>
/// Creates an EmphasisItalicPayload.
/// </summary>
/// <param name="enabled">Whether italics formatting should be enabled or disabled for following text.</param>
public EmphasisItalicPayload(bool enabled)
{
IsEnabled = enabled;
}
public override string ToString()
{
return $"{Type} - Enabled: {IsEnabled}";
}
protected override byte[] EncodeImpl()
{
// realistically this will always be a single byte of value 1 or 2
// but we'll treat it normally anyway
var enabledBytes = MakeInteger(IsEnabled ? (uint)1 : 0);
var chunkLen = enabledBytes.Length + 1;
var bytes = new List<byte>()
{
START_BYTE, (byte)SeStringChunkType.EmphasisItalic, (byte)chunkLen
};
bytes.AddRange(enabledBytes);
bytes.Add(END_BYTE);
return bytes.ToArray();
}
protected override void DecodeImpl(BinaryReader reader, long endOfStream)
{
IsEnabled = (GetInteger(reader) == 1);
}
}
}

View file

@ -1,48 +1,98 @@
using Dalamud.Data.TransientSheet;
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
{
/// <summary>
/// An SeString Payload representing an interactable item link.
/// </summary>
public class ItemPayload : Payload
{
public override PayloadType Type => PayloadType.Item;
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(uint itemId, bool isHQ)
private Item item;
/// <summary>
/// The underlying Lumina Item represented by this payload.
/// </summary>
/// <remarks>
/// Value is evaluated lazily and cached.
/// </remarks>
public Item Item
{
ItemId = itemId;
IsHQ = isHQ;
}
public override void Resolve()
{
if (string.IsNullOrEmpty(ItemName))
get
{
dynamic item = XivApi.GetItem((int)ItemId).GetAwaiter().GetResult();
ItemName = item.Name;
this.item ??= this.dataResolver.GetExcelSheet<Item>().GetRow((int)this.itemId);
return this.item;
}
}
public override byte[] Encode()
// mainly to allow overriding the name (for things like owo)
// TODO: even though this is present in some item links, it may not really have a use at all
// For things like owo, changing the text payload is probably correct, whereas changing the
// actual embedded name might not work properly.
private string displayName = null;
/// <summary>
/// The displayed name for this item link. Note that incoming links only sometimes have names embedded,
/// often the name is only present in a following text payload.
/// </summary>
public string DisplayName
{
var actualItemId = IsHQ ? ItemId + 1000000 : ItemId;
get
{
return this.displayName;
}
set
{
this.displayName = value;
Dirty = true;
}
}
/// <summary>
/// Whether or not this item link is for a high-quality version of the item.
/// </summary>
public bool IsHQ { get; private set; } = false;
private uint itemId;
internal ItemPayload() { }
/// <summary>
/// Creates a payload representing an interactable item link for the specified item.
/// </summary>
/// <param name="itemId">The id of the item.</param>
/// <param name="isHQ">Whether or not the link should be for the high-quality variant of the item.</param>
/// <param name="displayNameOverride">An optional name to include in the item link. Typically this should
/// be left as null, or set to the normal item name. Actual overrides are better done with the subsequent
/// TextPayload that is a part of a full item link in chat.</param>
public ItemPayload(uint itemId, bool isHQ, string displayNameOverride = null)
{
this.itemId = itemId;
this.IsHQ = isHQ;
this.displayName = displayNameOverride;
}
public override string ToString()
{
return $"{Type} - ItemId: {itemId}, IsHQ: {IsHQ}, Name: {this.displayName ?? Item.Name}";
}
protected override byte[] EncodeImpl()
{
var actualItemId = IsHQ ? this.itemId + 1000000 : this.itemId;
var idBytes = MakeInteger(actualItemId);
bool hasName = !string.IsNullOrEmpty(ItemName);
bool hasName = !string.IsNullOrEmpty(this.displayName);
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
chunkLen += (1 + 1 + ItemName.Length);
chunkLen += (1 + 1 + this.displayName.Length);
if (IsHQ)
{
chunkLen += 4; // unicode representation of the HQ symbol is 3 bytes, preceded by a space
@ -61,7 +111,7 @@ namespace Dalamud.Game.Chat.SeStringHandling.Payloads
// Links don't have to include the name, but if they do, it requires additional work
if (hasName)
{
var nameLen = ItemName.Length + 1;
var nameLen = this.displayName.Length + 1;
if (IsHQ)
{
nameLen += 4; // space plus 3 bytes for HQ symbol
@ -72,7 +122,7 @@ namespace Dalamud.Game.Chat.SeStringHandling.Payloads
0xFF, // unk
(byte)nameLen
});
bytes.AddRange(Encoding.UTF8.GetBytes(ItemName));
bytes.AddRange(Encoding.UTF8.GetBytes(this.displayName));
if (IsHQ)
{
@ -86,18 +136,13 @@ namespace Dalamud.Game.Chat.SeStringHandling.Payloads
return bytes.ToArray();
}
public override string ToString()
protected override void DecodeImpl(BinaryReader reader, long endOfStream)
{
return $"{Type} - ItemId: {ItemId}, ItemName: {ItemName}, IsHQ: {IsHQ}";
}
this.itemId = GetInteger(reader);
protected override void ProcessChunkImpl(BinaryReader reader, long endOfStream)
{
ItemId = GetInteger(reader);
if (ItemId > 1000000)
if (this.itemId > 1000000)
{
ItemId -= 1000000;
this.itemId -= 1000000;
IsHQ = true;
}
@ -109,6 +154,11 @@ namespace Dalamud.Game.Chat.SeStringHandling.Payloads
var itemNameLen = (int)GetInteger(reader);
var itemNameBytes = reader.ReadBytes(itemNameLen);
// it probably isn't necessary to store this, as we now get the lumina Item
// on demand from the id, which will have the name
// For incoming links, the name "should?" always match
// but we'll store it for use in encode just in case it doesn't
// HQ items have the HQ symbol as part of the name, but since we already recorded
// the HQ flag, we want just the bare name
if (IsHQ)
@ -116,10 +166,28 @@ namespace Dalamud.Game.Chat.SeStringHandling.Payloads
itemNameBytes = itemNameBytes.Take(itemNameLen - 4).ToArray();
}
ItemName = Encoding.UTF8.GetString(itemNameBytes);
this.displayName = Encoding.UTF8.GetString(itemNameBytes);
}
}
protected override byte[] MakeInteger(uint value, bool withMarker = true, bool incrementSmallInts = true)
{
// TODO: as part of refactor
// linking an item id that is a multiple of 256 seemingly *requires* using byte*256 marker encoding
// or the link will not display correctly
// I am unsure if this applies to other data types as well, so keeping localized here, pending the
// refactor of all this integer handling mess
if (value % 256 == 0)
{
value /= 256;
// this is no longer a small int, but it was likely converted to that range
incrementSmallInts = false;
}
return base.MakeInteger(value, withMarker, incrementSmallInts);
}
protected override byte GetMarkerForIntegerBytes(byte[] bytes)
{
// custom marker just for hq items?
@ -128,6 +196,12 @@ namespace Dalamud.Game.Chat.SeStringHandling.Payloads
return (byte)IntegerType.Int24Special;
}
// TODO: as in the above function
if (bytes.Length == 1 && (this.itemId % 256 == 0))
{
return (byte)IntegerType.ByteTimes256;
}
return base.GetMarkerForIntegerBytes(bytes);
}
}

View file

@ -5,32 +5,179 @@ using System.IO;
namespace Dalamud.Game.Chat.SeStringHandling.Payloads
{
/// <summary>
/// An SeString Payload representing an interactable map position link.
/// </summary>
public class MapLinkPayload : Payload
{
public override PayloadType Type => PayloadType.MapLink;
// pre-Resolve() values
public uint TerritoryTypeId { get; set; }
public uint MapId { get; set; }
public uint RawX { get; set; }
public uint RawY { get; set; }
private Map map;
/// <summary>
/// The Map specified for this map link.
/// </summary>
/// <remarks>
/// Value is evaluated lazily and cached.
/// </remarks>
public Map Map
{
get
{
this.map ??= this.dataResolver.GetExcelSheet<Map>().GetRow((int)this.mapId);
return this.map;
}
}
// 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; }
private TerritoryType territoryType;
/// <summary>
/// The TerritoryType specified for this map link.
/// </summary>
/// <remarks>
/// Value is evaluated lazily and cached.
/// </remarks>
public TerritoryType TerritoryType
{
get
{
this.territoryType ??= this.dataResolver.GetExcelSheet<TerritoryType>().GetRow((int)this.territoryTypeId);
return this.territoryType;
}
}
/// <summary>
/// The internal x-coordinate for this map position.
/// </summary>
public int RawX { get; private set; }
/// <summary>
/// The internal y-coordinate for this map position.
/// </summary>
public int RawY { get; private set; }
// these could be cached, but this isn't really too egregious
/// <summary>
/// The readable x-coordinate position for this map link. This value is approximate and unrounded.
/// </summary>
public float XCoord
{
get
{
return ConvertRawPositionToMapCoordinate(RawX, Map.SizeFactor);
}
}
/// <summary>
/// The readable y-coordinate position for this map link. This value is approximate and unrounded.
/// </summary>
public float YCoord
{
get
{
return ConvertRawPositionToMapCoordinate(RawY, Map.SizeFactor);
}
}
/// <summary>
/// The printable map coordinates for this link. This value tries to match the in-game printable text as closely as possible
/// but is an approximation and may be slightly off for some positions.
/// </summary>
public string CoordinateString
{
get
{
// this truncates the values to one decimal without rounding, which is what the game does
// the fudge also just attempts to correct the truncated/displayed value for rounding/fp issues
// TODO: should this fudge factor be the same as in the ctor? currently not since that is customizable
const float fudge = 0.02f;
var x = Math.Truncate((XCoord+fudge) * 10.0f) / 10.0f;
var y = Math.Truncate((YCoord+fudge) * 10.0f) / 10.0f;
// the formatting and spacing the game uses
return $"( {x.ToString("0.0")} , {y.ToString("0.0")} )";
}
}
private string placeNameRegion;
/// <summary>
/// The region name for this map link. This corresponds to the upper zone name found in the actual in-game map UI. eg, "La Noscea"
/// </summary>
public string PlaceNameRegion
{
get
{
this.placeNameRegion ??= this.dataResolver.GetExcelSheet<PlaceName>().GetRow(TerritoryType.PlaceNameRegion).Name;
return this.placeNameRegion;
}
}
private string placeName;
/// <summary>
/// The place name for this map link. This corresponds to the lower zone name found in the actual in-game map UI. eg, "Limsa Lominsa Upper Decks"
/// </summary>
public string PlaceName
{
get
{
this.placeName ??= this.dataResolver.GetExcelSheet<PlaceName>().GetRow(TerritoryType.PlaceName).Name;
return this.placeName;
}
}
/// <summary>
/// The data string for this map link, for use by internal game functions that take a string variant and not a binary payload.
/// </summary>
public string DataString => $"m:{TerritoryType.RowId},{Map.RowId},{RawX},{RawY}";
private uint territoryTypeId;
private uint mapId;
// 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
internal MapLinkPayload() { }
var packedTerritoryAndMapBytes = MakePackedInteger(TerritoryTypeId, MapId);
var xBytes = MakeInteger(RawX);
var yBytes = MakeInteger(RawY);
/// <summary>
/// Creates an interactable MapLinkPayload from a human-readable position.
/// </summary>
/// <param name="territoryTypeId">The id of the TerritoryType entry for this link.</param>
/// <param name="mapId">The id of the Map entry for this link.</param>
/// <param name="niceXCoord">The human-readable x-coordinate for this link.</param>
/// <param name="niceYCoord">The human-readable y-coordinate for this link.</param>
/// <param name="fudgeFactor">An optional offset to account for rounding and truncation errors; it is best to leave this untouched in most cases.</param>
public MapLinkPayload(uint territoryTypeId, uint mapId, float niceXCoord, float niceYCoord, float fudgeFactor = 0.05f)
{
this.territoryTypeId = territoryTypeId;
this.mapId = mapId;
// this fudge is necessary basically to ensure we don't shift down a full tenth
// because essentially values are truncated instead of rounded, so 3.09999f will become
// 3.0f and not 3.1f
RawX = this.ConvertMapCoordinateToRawPosition(niceXCoord + fudgeFactor, Map.SizeFactor);
RawY = this.ConvertMapCoordinateToRawPosition(niceYCoord + fudgeFactor, Map.SizeFactor);
}
/// <summary>
/// Creates an interactable MapLinkPayload from a raw position.
/// </summary>
/// <param name="territoryTypeId">The id of the TerritoryType entry for this link.</param>
/// <param name="mapId">The id of the Map entry for this link.</param>
/// <param name="rawX">The internal raw x-coordinate for this link.</param>
/// <param name="rawY">The internal raw y-coordinate for this link.</param>
public MapLinkPayload(uint territoryTypeId, uint mapId, int rawX, int rawY)
{
this.territoryTypeId = territoryTypeId;
this.mapId = mapId;
RawX = rawX;
RawY = rawY;
}
public override string ToString()
{
return $"{Type} - TerritoryTypeId: {territoryTypeId}, MapId: {mapId}, RawX: {RawX}, RawY: {RawY}, display: {PlaceName} {CoordinateString}";
}
protected override byte[] EncodeImpl()
{
var packedTerritoryAndMapBytes = MakePackedInteger(this.territoryTypeId, this.mapId);
var xBytes = MakeInteger(unchecked((uint)RawX));
var yBytes = MakeInteger(unchecked((uint)RawY));
var chunkLen = 4 + packedTerritoryAndMapBytes.Length + xBytes.Length + yBytes.Length;
@ -50,27 +197,7 @@ namespace Dalamud.Game.Chat.SeStringHandling.Payloads
return bytes.ToArray();
}
public override void Resolve()
{
// TODO: add once lumina DI is figured out
//if (string.IsNullOrEmpty(Territory))
//{
// var terrRow = dataResolver.GetExcelSheet<TerritoryType>().GetRow((int)TerritoryTypeId);
// Territory = dataResolver.GetExcelSheet<PlaceName>().GetRow(terrRow.PlaceName).Name;
// Zone = dataResolver.GetExcelSheet<PlaceName>().GetRow(terrRow.PlaceNameZone).Name;
// var mapSizeFactor = dataResolver.GetExcelSheet<Map>().GetRow((int)MapId).SizeFactor;
// XCoord = ConvertRawPositionToMapCoordinate(RawX, mapSizeFactor);
// YCoord = ConvertRawPositionToMapCoordinate(RawY, mapSizeFactor);
//}
}
public override string ToString()
{
return $"{Type} - TerritoryTypeId: {TerritoryTypeId}, MapId: {MapId}, RawX: {RawX}, RawY: {RawY}";
}
protected override void ProcessChunkImpl(BinaryReader reader, long endOfStream)
protected override void DecodeImpl(BinaryReader reader, long endOfStream)
{
// for debugging for now
var oldPos = reader.BaseStream.Position;
@ -79,9 +206,9 @@ namespace Dalamud.Game.Chat.SeStringHandling.Payloads
try
{
(TerritoryTypeId, MapId) = GetPackedIntegers(reader);
RawX = (uint)GetInteger(reader);
RawY = (uint)GetInteger(reader);
(this.territoryTypeId, this.mapId) = GetPackedIntegers(reader);
RawX = unchecked((int)GetInteger(reader));
RawY = unchecked((int)GetInteger(reader));
// the Z coordinate is never in this chunk, just the text (if applicable)
// seems to always be FF 01
@ -98,23 +225,23 @@ namespace Dalamud.Game.Chat.SeStringHandling.Payloads
#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)
private float ConvertRawPositionToMapCoordinate(int pos, float scale)
{
var c = scale / 100.0f;
var scaledPos = (int)pos * c / 1000.0f;
var scaledPos = 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)
private int 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);
return (int)scaledPos;
}
#endregion

View file

@ -1,56 +1,95 @@
using Lumina.Excel.GeneratedSheets;
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
{
/// <summary>
/// An SeString Payload representing a player link.
/// </summary>
public class PlayerPayload : Payload
{
public override PayloadType Type => PayloadType.Player;
public string PlayerName { get; private set; }
public uint ServerId { get; private set; }
public string ServerName { get; private set; } = String.Empty;
public PlayerPayload() { }
public PlayerPayload(string playerName, uint serverId)
private string playerName;
/// <summary>
/// The player's displayed name. This does not contain the server name.
/// </summary>
public string PlayerName
{
PlayerName = playerName;
ServerId = serverId;
}
public override void Resolve()
{
if (string.IsNullOrEmpty(ServerName))
get { return this.playerName; }
set
{
dynamic server = XivApi.Get($"World/{ServerId}").GetAwaiter().GetResult();
ServerName = server.Name;
this.playerName = value;
Dirty = true;
}
}
public override byte[] Encode()
private World world;
/// <summary>
/// The Lumina object representing the player's home server.
/// </summary>
/// <remarks>
/// Value is evaluated lazily and cached.
/// </remarks>
public World World
{
var chunkLen = PlayerName.Length + 7;
get
{
this.world ??= this.dataResolver.GetExcelSheet<World>().GetRow((int)this.serverId);
return this.world;
}
}
/// <summary>
/// A text representation of this player link matching how it might appear in-game.
/// The world name will always be present.
/// </summary>
public string DisplayedName => $"{PlayerName}{(char)SeIconChar.CrossWorld}{World.Name}";
private uint serverId;
internal PlayerPayload() { }
/// <summary>
/// Create a PlayerPayload link for the specified player.
/// </summary>
/// <param name="playerName">The player's displayed name.</param>
/// <param name="serverId">The player's home server id.</param>
public PlayerPayload(string playerName, uint serverId)
{
this.playerName = playerName;
this.serverId = serverId;
}
public override string ToString()
{
return $"{Type} - PlayerName: {PlayerName}, ServerId: {serverId}, ServerName: {World.Name}";
}
protected override byte[] EncodeImpl()
{
var chunkLen = this.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
(byte)(this.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)
(byte)(this.playerName.Length+1)
};
bytes.AddRange(Encoding.UTF8.GetBytes(PlayerName));
bytes.AddRange(Encoding.UTF8.GetBytes(this.playerName));
bytes.Add(END_BYTE);
// TODO: should these really be here? additional payloads should come in separately already...
// 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());
bytes.AddRange(new TextPayload(playerName).Encode());
// unsure about this entire packet, but it seems to always follow a name
bytes.AddRange(new byte[]
@ -63,23 +102,18 @@ namespace Dalamud.Game.Chat.SeStringHandling.Payloads
return bytes.ToArray();
}
public override string ToString()
{
return $"{Type} - PlayerName: {PlayerName}, ServerId: {ServerId}, ServerName: {ServerName}";
}
protected override void ProcessChunkImpl(BinaryReader reader, long endOfStream)
protected override void DecodeImpl(BinaryReader reader, long endOfStream)
{
// unk
reader.ReadByte();
ServerId = GetInteger(reader);
this.serverId = GetInteger(reader);
// unk
reader.ReadBytes(2);
var nameLen = (int)GetInteger(reader);
PlayerName = Encoding.UTF8.GetString(reader.ReadBytes(nameLen));
this.playerName = Encoding.UTF8.GetString(reader.ReadBytes(nameLen));
}
}
}

View file

@ -1,51 +1,84 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
namespace Dalamud.Game.Chat.SeStringHandling.Payloads
{
/// <summary>
/// An SeString Payload representing unhandled raw payload data.
/// Mainly useful for constructing unhandled hardcoded payloads, or forwarding any unknown
/// payloads without modification.
/// </summary>
public class RawPayload : Payload
{
// this and others could be an actual static member somewhere and avoid construction costs, but that probably isn't a real concern
/// <summary>
/// A fixed Payload representing a common link-termination sequence, found in many payload chains.
/// </summary>
public static RawPayload LinkTerminator => new RawPayload(new byte[] { 0x02, 0x27, 0x07, 0xCF, 0x01, 0x01, 0x01, 0xFF, 0x01, 0x03 });
public override PayloadType Type => PayloadType.Unknown;
public byte ChunkType { get; private set; }
public byte[] Data { get; private set; }
public RawPayload(byte chunkType)
private byte[] data;
// this is a bit different from the underlying data
// We need to store just the chunk data for decode to behave nicely, but when reading data out
// it makes more sense to get the entire payload
/// <summary>
/// The entire payload byte sequence for this payload.
/// The returned data is a clone and modifications will not be persisted.
/// </summary>
public byte[] Data
{
ChunkType = chunkType;
get
{
// for now don't allow modifying the contents
// because we don't really have a way to track Dirty
return (byte[])Encode().Clone();
}
}
public override void Resolve()
private byte chunkType;
internal RawPayload(byte chunkType)
{
// nothing to do
this.chunkType = chunkType;
}
public override byte[] Encode()
public RawPayload(byte[] data)
{
var chunkLen = Data.Length + 1;
// this payload is 'special' in that we require the entire chunk to be passed in
// and not just the data after the header
// This sets data to hold the chunk data fter the header, excluding the END_BYTE
this.chunkType = data[1];
this.data = data.Skip(3).Take(data.Length-4).ToArray();
}
public override string ToString()
{
return $"{Type} - Data: {BitConverter.ToString(Data).Replace("-", " ")}";
}
protected override byte[] EncodeImpl()
{
var chunkLen = this.data.Length + 1;
var bytes = new List<byte>()
{
START_BYTE,
ChunkType,
this.chunkType,
(byte)chunkLen
};
bytes.AddRange(Data);
bytes.AddRange(this.data);
bytes.Add(END_BYTE);
return bytes.ToArray();
}
public override string ToString()
protected override void DecodeImpl(BinaryReader reader, long endOfStream)
{
return $"{Type} - Chunk type: {ChunkType:X}, Data: {BitConverter.ToString(Data).Replace("-", " ")}";
}
protected override void ProcessChunkImpl(BinaryReader reader, long endOfStream)
{
Data = reader.ReadBytes((int)(endOfStream - reader.BaseStream.Position + 1));
this.data = reader.ReadBytes((int)(endOfStream - reader.BaseStream.Position + 1));
}
}
}

View file

@ -1,40 +1,54 @@
using Lumina.Excel.GeneratedSheets;
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
{
/// <summary>
/// An SeString Payload representing an interactable status link.
/// </summary>
public class StatusPayload : Payload
{
public override PayloadType Type => PayloadType.Status;
public uint StatusId { get; private set; }
public string StatusName { get; private set; } = string.Empty;
public StatusPayload() { }
public StatusPayload(uint statusId)
private Status status;
/// <summary>
/// The Lumina Status object represented by this payload.
/// </summary>
/// <remarks>
/// Value is evaluated lazily and cached.
/// </remarks>
public Status Status
{
StatusId = statusId;
}
public override void Resolve()
{
if (string.IsNullOrEmpty(StatusName))
get
{
dynamic status = XivApi.Get($"Status/{StatusId}").GetAwaiter().GetResult();
//Console.WriteLine($"Resolved status {StatusId} to {status.Name}");
StatusName = status.Name;
status ??= this.dataResolver.GetExcelSheet<Status>().GetRow((int)this.statusId);
return status;
}
}
public override byte[] Encode()
private uint statusId;
internal StatusPayload() { }
/// <summary>
/// Creates a new StatusPayload for the given status id.
/// </summary>
/// <param name="statusId">The id of the Status for this link.</param>
public StatusPayload(uint statusId)
{
var idBytes = MakeInteger(StatusId);
this.statusId = statusId;
}
public override string ToString()
{
return $"{Type} - StatusId: {statusId}, Name: {Status.Name}";
}
protected override byte[] EncodeImpl()
{
var idBytes = MakeInteger(this.statusId);
var chunkLen = idBytes.Length + 7;
var bytes = new List<byte>()
@ -49,14 +63,9 @@ namespace Dalamud.Game.Chat.SeStringHandling.Payloads
return bytes.ToArray();
}
public override string ToString()
protected override void DecodeImpl(BinaryReader reader, long endOfStream)
{
return $"{Type} - StatusId: {StatusId}, StatusName: {StatusName}";
}
protected override void ProcessChunkImpl(BinaryReader reader, long endOfStream)
{
StatusId = GetInteger(reader);
this.statusId = GetInteger(reader);
}
}
}

View file

@ -1,60 +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 TextPayload : Payload
/// <summary>
/// An SeString Payload representing a plain text string.
/// </summary>
public class TextPayload : Payload, ITextProvider
{
public override PayloadType Type => PayloadType.RawText;
private string textConverted = null;
// allow modifying the text of existing payloads on the fly
private string text;
/// <summary>
/// The Text of this text payload as an UTF-8 converted string.
/// Don't rely on this for accurate representation of SE payload data, please check RawData instead.
/// The text contained in this payload.
/// This may contain SE's special unicode characters.
/// </summary>
public string Text {
get { return this.textConverted ??= Encoding.UTF8.GetString(RawData); }
set {
this.textConverted = value;
RawData = Encoding.UTF8.GetBytes(value);
public string Text
{
get { return this.text; }
set
{
this.text = value;
Dirty = true;
}
}
/// <summary>
/// The raw unconverted data of this text payload.
/// </summary>
public byte[] RawData { get; 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)
internal TextPayload() { }
/// <summary>
/// Creates a new TextPayload for the given text.
/// </summary>
/// <param name="text">The text to include for this payload.</param>
public TextPayload(string text)
{
var text = new List<byte>();
this.text = text;
}
protected override byte[] EncodeImpl()
{
// special case to allow for empty text payloads, so users don't have to check
// this may change or go away
if (string.IsNullOrEmpty(this.text))
{
return new byte[] { };
}
return Encoding.UTF8.GetBytes(this.text);
}
protected override void DecodeImpl(BinaryReader reader, long endOfStream)
{
var textBytes = new List<byte>();
while (reader.BaseStream.Position < endOfStream)
{
@ -66,13 +70,13 @@ namespace Dalamud.Game.Chat.SeStringHandling.Payloads
break;
}
text.Add(nextByte);
textBytes.Add(nextByte);
}
if (text.Count > 0)
if (textBytes.Count > 0)
{
// TODO: handling of the game's assorted special unicode characters
Text = Encoding.UTF8.GetString(text.ToArray());
this.text = Encoding.UTF8.GetString(textBytes.ToArray());
}
}
}

View file

@ -1,22 +1,89 @@
using Lumina.Excel.GeneratedSheets;
using System;
using System.Collections.Generic;
using System.IO;
namespace Dalamud.Game.Chat.SeStringHandling.Payloads
{
/// <summary>
/// An SeString Payload representing a UI foreground color applied to following text payloads.
/// </summary>
public class UIForegroundPayload : Payload
{
/// <summary>
/// Payload representing disabling foreground color on following text.
/// </summary>
public static UIForegroundPayload UIForegroundOff => new UIForegroundPayload(0);
public override PayloadType Type => PayloadType.UIForeground;
public ushort RawColor { get; private set; }
/// <summary>
/// Whether or not this payload represents applying a foreground color, or disabling one.
/// </summary>
public bool IsEnabled => ColorKey != 0;
//public int Red { get; private set; }
//public int Green { get; private set; }
//public int Blue { get; private set; }
public override byte[] Encode()
private UIColor color;
/// <summary>
/// A Lumina UIColor object representing this payload. The actual color data is at UIColor.UIForeground
/// </summary>
/// <remarks>
/// Value is evaluated lazily and cached.
/// </remarks>
public UIColor UIColor
{
var colorBytes = MakeInteger(RawColor);
get
{
this.color ??= this.dataResolver.GetExcelSheet<UIColor>().GetRow(this.colorKey);
return this.color;
}
}
/// <summary>
/// The color key used as a lookup in the UIColor table for this foreground color.
/// </summary>
public ushort ColorKey
{
get { return this.colorKey; }
set
{
this.colorKey = value;
this.color = null;
Dirty = true;
}
}
/// <summary>
/// The Red/Green/Blue values for this foreground color, encoded as a typical hex color.
/// </summary>
public uint RGB
{
get
{
return (UIColor.UIForeground & 0xFFFFFF);
}
}
private ushort colorKey;
internal UIForegroundPayload() { }
/// <summary>
/// Creates a new UIForegroundPayload for the given UIColor key.
/// </summary>
/// <param name="colorKey"></param>
public UIForegroundPayload(ushort colorKey)
{
this.colorKey = colorKey;
}
public override string ToString()
{
return $"{Type} - UIColor: {colorKey} color: {(IsEnabled ? RGB : 0)}";
}
protected override byte[] EncodeImpl()
{
var colorBytes = MakeInteger(this.colorKey);
var chunkLen = colorBytes.Length + 1;
var bytes = new List<byte>(new byte[]
@ -30,19 +97,9 @@ namespace Dalamud.Game.Chat.SeStringHandling.Payloads
return bytes.ToArray();
}
public override void Resolve()
protected override void DecodeImpl(BinaryReader reader, long endOfStream)
{
// 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);
this.colorKey = (ushort)GetInteger(reader);
}
protected override byte GetMarkerForIntegerBytes(byte[] bytes)

View file

@ -1,22 +1,89 @@
using Lumina.Excel.GeneratedSheets;
using System;
using System.Collections.Generic;
using System.IO;
namespace Dalamud.Game.Chat.SeStringHandling.Payloads
{
/// <summary>
/// An SeString Payload representing a UI glow color applied to following text payloads.
/// </summary>
public class UIGlowPayload : Payload
{
/// <summary>
/// Payload representing disabling glow color on following text.
/// </summary>
public static UIGlowPayload UIGlowOff => new UIGlowPayload(0);
public override PayloadType Type => PayloadType.UIGlow;
public ushort RawColor { get; private set; }
/// <summary>
/// Whether or not this payload represents applying a glow color, or disabling one.
/// </summary>
public bool IsEnabled => ColorKey != 0;
//public int Red { get; private set; }
//public int Green { get; private set; }
//public int Blue { get; private set; }
public override byte[] Encode()
private UIColor color;
/// <summary>
/// A Lumina UIColor object representing this payload. The actual color data is at UIColor.UIGlow
/// </summary>
/// <remarks>
/// Value is evaluated lazily and cached.
/// </remarks>
public UIColor UIColor
{
var colorBytes = MakeInteger(RawColor);
get
{
this.color ??= this.dataResolver.GetExcelSheet<UIColor>().GetRow(this.colorKey);
return this.color;
}
}
/// <summary>
/// The color key used as a lookup in the UIColor table for this glow color.
/// </summary>
public ushort ColorKey
{
get { return this.colorKey; }
set
{
this.colorKey = value;
this.color = null;
Dirty = true;
}
}
/// <summary>
/// The Red/Green/Blue values for this glow color, encoded as a typical hex color.
/// </summary>
public uint RGB
{
get
{
return (UIColor.UIGlow & 0xFFFFFF);
}
}
private ushort colorKey;
internal UIGlowPayload() { }
/// <summary>
/// Creates a new UIForegroundPayload for the given UIColor key.
/// </summary>
/// <param name="colorKey"></param>
public UIGlowPayload(ushort colorKey)
{
this.colorKey = colorKey;
}
public override string ToString()
{
return $"{Type} - UIColor: {colorKey} color: {(IsEnabled ? RGB : 0)}";
}
protected override byte[] EncodeImpl()
{
var colorBytes = MakeInteger(this.colorKey);
var chunkLen = colorBytes.Length + 1;
var bytes = new List<byte>(new byte[]
@ -30,19 +97,9 @@ namespace Dalamud.Game.Chat.SeStringHandling.Payloads
return bytes.ToArray();
}
public override void Resolve()
protected override void DecodeImpl(BinaryReader reader, long endOfStream)
{
// 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);
this.colorKey = (ushort)GetInteger(reader);
}
protected override byte GetMarkerForIntegerBytes(byte[] bytes)

View file

@ -3,9 +3,6 @@ using System.Collections.Generic;
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
{
@ -14,12 +11,13 @@ namespace Dalamud.Game.Chat.SeStringHandling
/// </summary>
public class SeString
{
public List<Payload> Payloads { get; }
// TODO: probably change how this is done/where it comes from
internal static Dalamud Dalamud { get; set; }
public SeString(List<Payload> payloads)
{
Payloads = payloads;
}
/// <summary>
/// The ordered list of payloads included in this SeString.
/// </summary>
public List<Payload> Payloads { get; }
/// <summary>
/// Helper function to get all raw text from a message as a single joined string
@ -29,35 +27,30 @@ namespace Dalamud.Game.Chat.SeStringHandling
/// </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();
get
{
return Payloads
.Where(p => p is ITextProvider)
.Cast<ITextProvider>()
.Aggregate(new StringBuilder(), (sb, tp) => sb.Append(tp.Text), sb => sb.ToString());
}
}
/// <summary>
/// Parse an array of bytes to a SeString.
/// Parse a binary game message into an SeString.
/// </summary>
/// <param name="bytes"></param>
/// <returns></returns>
/// <param name="bytes">Binary message payload data in SE's internal format.</param>
/// <returns>An SeString containing parsed Payload objects for each payload in the data.</returns>
public static SeString Parse(byte[] bytes)
{
var payloads = new List<Payload>();
using (var stream = new MemoryStream(bytes)) {
using var reader = new BinaryReader(stream);
using (var stream = new MemoryStream(bytes))
using (var reader = new BinaryReader(stream))
{
while (stream.Position < bytes.Length)
{
var payload = Payload.Process(reader);
var payload = Payload.Decode(reader);
if (payload != null)
payloads.Add(payload);
}
@ -67,10 +60,61 @@ namespace Dalamud.Game.Chat.SeStringHandling
}
/// <summary>
/// Encode a parsed/created SeString to an array of bytes, to be used for injection.
/// Creates a new SeString from an ordered list of payloads.
/// </summary>
/// <param name="payloads"></param>
/// <returns>The bytes of the message.</returns>
/// <param name="payloads">The Payload objects to make up this string.</param>
public SeString(List<Payload> payloads)
{
Payloads = payloads;
}
/// <summary>
/// Creates a new SeString from an ordered list of payloads.
/// </summary>
/// <param name="payloads">The Payload objects to make up this string.</param>
public SeString(Payload[] payloads)
{
Payloads = new List<Payload>(payloads);
}
/// <summary>
/// Appends the contents of one SeString to this one.
/// </summary>
/// <param name="other">The SeString to append to this one.</param>
/// <returns>This object.</returns>
public SeString Append(SeString other)
{
Payloads.AddRange(other.Payloads);
return this;
}
/// <summary>
/// Appends a list of payloads to this SeString.
/// </summary>
/// <param name="payloads">The Payloads to append.</param>
/// <returns>This object.</returns>
public SeString Append(List<Payload> payloads)
{
Payloads.AddRange(payloads);
return this;
}
/// <summary>
/// Appends a single payload to this SeString.
/// </summary>
/// <param name="payload">The payload to append.</param>
/// <returns>This object.</returns>
public SeString Append(Payload payload)
{
Payloads.Add(payload);
return this;
}
/// <summary>
/// Encodes the Payloads in this SeString into a binary representation
/// suitable for use by in-game handlers, such as the chat log.
/// </summary>
/// <returns>The binary encoded payload data.</returns>
public byte[] Encode()
{
var messageBytes = new List<byte>();

View file

@ -0,0 +1,149 @@
using Dalamud.Game.Chat.SeStringHandling.Payloads;
using Lumina.Excel.GeneratedSheets;
using System;
using System.Collections.Generic;
using System.Linq;
using DalamudItem = Dalamud.Data.TransientSheet.Item;
namespace Dalamud.Game.Chat.SeStringHandling
{
/// <summary>
/// A utility class for working with common SeString variants.
/// </summary>
public static class SeStringUtils
{
/// <summary>
/// Creates an SeString representing an entire Payload chain that can be used to link an item in the chat log.
/// </summary>
/// <param name="itemId">The id of the item to link.</param>
/// <param name="isHQ">Whether to link the high-quality variant of the item.</param>
/// <param name="displayNameOverride">An optional name override to display, instead of the actual item name.</param>
/// <returns>An SeString containing all the payloads necessary to display an item link in the chat log.</returns>
public static SeString CreateItemLink(uint itemId, bool isHQ, string displayNameOverride = null)
{
string displayName = displayNameOverride ?? SeString.Dalamud.Data.GetExcelSheet<DalamudItem>().GetRow((int)itemId).Name;
if (isHQ)
{
displayName += $" {(char)SeIconChar.HighQuality}";
}
// TODO: probably a cleaner way to build these than doing the bulk+insert
var payloads = new List<Payload>(new Payload[]
{
new UIForegroundPayload(0x0225),
new UIGlowPayload(0x0226),
new ItemPayload(itemId, isHQ),
// arrow goes here
new TextPayload(displayName),
RawPayload.LinkTerminator
// sometimes there is another set of uiglow/foreground off payloads here
// might be necessary when including additional text after the item name
});
payloads.InsertRange(3, TextArrowPayloads());
return new SeString(payloads);
}
/// <summary>
/// Creates an SeString representing an entire Payload chain that can be used to link an item in the chat log.
/// </summary>
/// <param name="item">The Lumina Item to link.</param>
/// <param name="isHQ">Whether to link the high-quality variant of the item.</param>
/// <param name="displayNameOverride">An optional name override to display, instead of the actual item name.</param>
/// <returns>An SeString containing all the payloads necessary to display an item link in the chat log.</returns>
public static SeString CreateItemLink(DalamudItem item, bool isHQ, string displayNameOverride = null)
{
return CreateItemLink((uint)item.RowId, isHQ, displayNameOverride ?? item.Name);
}
public static SeString CreateMapLink(uint territoryId, uint mapId, int rawX, int rawY)
{
var mapPayload = new MapLinkPayload(territoryId, mapId, rawX, rawY);
var nameString = $"{mapPayload.PlaceName} {mapPayload.CoordinateString}";
var payloads = new List<Payload>(new Payload[]
{
mapPayload,
// arrow goes here
new TextPayload(nameString),
RawPayload.LinkTerminator
});
payloads.InsertRange(1, TextArrowPayloads());
return new SeString(payloads);
}
/// <summary>
/// Creates an SeString representing an entire Payload chain that can be used to link a map position in the chat log.
/// </summary>
/// <param name="territoryId">The id of the TerritoryType for this map link.</param>
/// <param name="mapId">The id of the Map for this map link.</param>
/// <param name="xCoord">The human-readable x-coordinate for this link.</param>
/// <param name="yCoord">The human-readable y-coordinate for this link.</param>
/// <param name="fudgeFactor">An optional offset to account for rounding and truncation errors; it is best to leave this untouched in most cases.</param>
/// <returns>An SeString containing all of the payloads necessary to display a map link in the chat log.</returns>
public static SeString CreateMapLink(uint territoryId, uint mapId, float xCoord, float yCoord, float fudgeFactor = 0.05f)
{
var mapPayload = new MapLinkPayload(territoryId, mapId, xCoord, yCoord, fudgeFactor);
var nameString = $"{mapPayload.PlaceName} {mapPayload.CoordinateString}";
var payloads = new List<Payload>(new Payload[]
{
mapPayload,
// arrow goes here
new TextPayload(nameString),
RawPayload.LinkTerminator
});
payloads.InsertRange(1, TextArrowPayloads());
return new SeString(payloads);
}
/// <summary>
/// Creates an SeString representing an entire Payload chain that can be used to link a map position in the chat log, matching a specified zone name.
/// </summary>
/// <param name="placeName">The name of the location for this link. This should be exactly the name as seen in a displayed map link in-game for the same zone.</param>
/// <param name="xCoord">The human-readable x-coordinate for this link.</param>
/// <param name="yCoord">The human-readable y-coordinate for this link.</param>
/// <param name="fudgeFactor">An optional offset to account for rounding and truncation errors; it is best to leave this untouched in most cases.</param>
/// <returns>An SeString containing all of the payloads necessary to display a map link in the chat log.</returns>
public static SeString CreateMapLink(string placeName, float xCoord, float yCoord, float fudgeFactor = 0.05f)
{
var mapSheet = SeString.Dalamud.Data.GetExcelSheet<Map>();
var matches = SeString.Dalamud.Data.GetExcelSheet<PlaceName>().GetRows()
.Where(row => row.Name.ToLowerInvariant() == placeName.ToLowerInvariant())
.ToArray();
foreach (var place in matches)
{
var map = mapSheet.GetRows().FirstOrDefault(row => row.PlaceName == place.RowId);
if (map != null)
{
return CreateMapLink(map.TerritoryType, (uint)map.RowId, xCoord, yCoord);
}
}
// TODO: empty? throw?
return null;
}
/// <summary>
/// Creates a list of Payloads necessary to display the arrow link marker icon in chat
/// with the appropriate glow and coloring.
/// </summary>
/// <returns>A list of all the payloads required to insert the link marker.</returns>
public static List<Payload> TextArrowPayloads()
{
return new List<Payload>(new Payload[]
{
new UIForegroundPayload(0x01F4),
new UIGlowPayload(0x01F5),
new TextPayload($"{(char)SeIconChar.LinkMarker}"),
UIGlowPayload.UIGlowOff,
UIForegroundPayload.UIForegroundOff
});
}
}
}

View file

@ -6,7 +6,7 @@ namespace Dalamud.Game.Chat
/// <summary>
/// The FFXIV chat types as seen in the LogKind ex table.
/// </summary>
public enum XivChatType : ushort
public enum XivChatType : ushort // FIXME: this is a single byte
{
None = 0,
Debug = 1,

View file

@ -188,14 +188,14 @@ namespace Dalamud.Game {
break;
}
Log.Debug($"Probable retainer sale: {message}, decoded item {itemLink.ItemId}, HQ {itemLink.IsHQ}");
Log.Debug($"Probable retainer sale: {message}, decoded item {itemLink.Item.RowId}, HQ {itemLink.IsHQ}");
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 var itemValue))
continue;
Task.Run(() => this.dalamud.BotManager.ProcessRetainerSale((int)itemLink.ItemId, itemValue, itemLink.IsHQ));
Task.Run(() => this.dalamud.BotManager.ProcessRetainerSale((int)itemLink.Item.RowId, itemValue, itemLink.IsHQ));
break;
}
}
@ -231,8 +231,16 @@ namespace Dalamud.Game {
}
private static string MakeItalics(string text) {
return Encoding.UTF8.GetString(new byte[] {0x02, 0x1A, 0x02, 0x02, 0x03}) + text +
Encoding.UTF8.GetString(new byte[] {0x02, 0x1A, 0x02, 0x01, 0x03});
// TODO: when the above code is switched to SeString, this can be a straight insertion of the
// italics payloads only, and be a lot cleaner
var italicString = new SeString(new List<Payload>(new Payload[]
{
EmphasisItalicPayload.ItalicsOn,
new TextPayload(text),
EmphasisItalicPayload.ItalicsOff
}));
return Encoding.UTF8.GetString(italicString.Encode());
}
}
}

View file

@ -138,8 +138,8 @@ namespace Dalamud.Game.Internal.Gui {
if (!FastByteArrayCompare(oldEdited, newEdited)) {
Log.Verbose("SeString was edited, taking precedence over StdString edit.");
message.RawData = newEdited;
}
//Log.Debug($"\nOLD: {BitConverter.ToString(originalMessageData)}\nNEW: {BitConverter.ToString(newEdited)}");
Log.Debug($"\nOLD: {BitConverter.ToString(originalMessageData)}\nNEW: {BitConverter.ToString(newEdited)}");
}
var messagePtr = pMessage;
OwnedStdString allocatedString = null;

View file

@ -172,8 +172,7 @@ namespace Dalamud.Game.Internal.Gui {
openMapWithFlag =
Address.GetVirtualFunction<OpenMapWithFlagDelegate>(uiMapObjectPtr, 0, 63);
var mapLinkString =
$"m:{mapLink.TerritoryTypeId},{mapLink.MapId},{unchecked((int)mapLink.RawX)},{unchecked((int)mapLink.RawY)}";
var mapLinkString = mapLink.DataString;
Log.Debug($"OpenMapWithMapLink: Opening Map Link: {mapLinkString}");