mirror of
https://github.com/goatcorp/Dalamud.git
synced 2025-12-12 10:17:22 +01:00
merge
This commit is contained in:
commit
de85271b73
23 changed files with 1646 additions and 384 deletions
|
|
@ -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;
|
||||
|
|
|
|||
53
Dalamud/Data/TransientSheet/Completion.cs
Normal file
53
Dalamud/Data/TransientSheet/Completion.cs
Normal 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 );
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
401
Dalamud/Data/TransientSheet/PetMirage.cs
Normal file
401
Dalamud/Data/TransientSheet/PetMirage.cs
Normal 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 );
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
9
Dalamud/Game/Chat/SeStringHandling/ITextProvider.cs
Normal file
9
Dalamud/Game/Chat/SeStringHandling/ITextProvider.cs
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
using System;
|
||||
|
||||
namespace Dalamud.Game.Chat.SeStringHandling
|
||||
{
|
||||
interface ITextProvider
|
||||
{
|
||||
string Text { get; }
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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>();
|
||||
|
|
|
|||
149
Dalamud/Game/Chat/SeStringHandling/SeStringUtils.cs
Normal file
149
Dalamud/Game/Chat/SeStringHandling/SeStringUtils.cs
Normal 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
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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}");
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue