514 lines
21 KiB
C#
Executable File
514 lines
21 KiB
C#
Executable File
using ChatTwo.Code;
|
|
using Dalamud.Game.Text.SeStringHandling;
|
|
using Dalamud.Game.Text.SeStringHandling.Payloads;
|
|
using System.Text;
|
|
using Lumina.Text.Payloads;
|
|
|
|
using PayloadType = Dalamud.Game.Text.SeStringHandling.PayloadType;
|
|
|
|
namespace ChatTwo.Util;
|
|
|
|
internal static class ChunkUtil
|
|
{
|
|
// internal static IEnumerable<Chunk> ToChunks(ReadOnlySeString msg, ChunkSource source, ChatType? defaultColour)
|
|
// {
|
|
// var chunks = new List<Chunk>();
|
|
//
|
|
// var italic = false;
|
|
// var foreground = new Stack<uint>();
|
|
// var glow = new Stack<uint>();
|
|
// Payload? link = null;
|
|
//
|
|
// void Append(string text)
|
|
// {
|
|
// chunks.Add(new TextChunk(source, link, text)
|
|
// {
|
|
// FallbackColour = defaultColour,
|
|
// Foreground = foreground.Count > 0 ? foreground.Peek() : null,
|
|
// Glow = glow.Count > 0 ? glow.Peek() : null,
|
|
// Italic = italic,
|
|
// });
|
|
// }
|
|
//
|
|
// foreach (var payload in msg)
|
|
// {
|
|
// if (payload.Type == ReadOnlySePayloadType.Text)
|
|
// {
|
|
// // We don't want to parse any null string
|
|
// var str = payload.ToString();
|
|
// var nulIndex = str.IndexOf('\0');
|
|
// if (nulIndex > 0)
|
|
// str = str[..nulIndex];
|
|
// if (string.IsNullOrEmpty(str))
|
|
// continue;
|
|
//
|
|
// Append(str);
|
|
// continue;
|
|
// }
|
|
//
|
|
// switch (payload.MacroCode)
|
|
// {
|
|
// case MacroCode.Italic:
|
|
// var newStatus = payload.TryGetExpression(out var expression) && expression.TryGetUInt(out var value) && value == 1;
|
|
// italic = newStatus;
|
|
// break;
|
|
// case MacroCode.Color:
|
|
// if (payload.TryGetExpression(out var eColor))
|
|
// {
|
|
// if (eColor.TryGetPlaceholderExpression(out var ph) && ph == (int)ExpressionType.StackColor)
|
|
// {
|
|
// if (foreground.Count > 0)
|
|
// foreground.Pop();
|
|
// }
|
|
// else if (TryResolveUInt(eColor, out var eColorVal))
|
|
// {
|
|
// var color = ColourUtil.ArgbToRgba(eColorVal);
|
|
//
|
|
// if (color > 0)
|
|
// foreground.Push(color);
|
|
// else if (foreground.Count > 0) // Push the previous color as we don't want invisible text
|
|
// foreground.Push(foreground.Peek());
|
|
// }
|
|
// }
|
|
// break;
|
|
// case MacroCode.EdgeColor:
|
|
// if (payload.TryGetExpression(out eColor))
|
|
// {
|
|
// if (eColor.TryGetPlaceholderExpression(out var ph) && ph == (int)ExpressionType.StackColor)
|
|
// {
|
|
// if (glow.Count > 0)
|
|
// glow.Pop();
|
|
// }
|
|
// else if (TryResolveUInt(eColor, out var eColorVal))
|
|
// {
|
|
// glow.Push(ColourUtil.ArgbToRgba(eColorVal));
|
|
// }
|
|
// }
|
|
// break;
|
|
// case MacroCode.ColorType:
|
|
// if (!payload.TryGetExpression(out var eColorType) || !eColorType.TryGetUInt(out var eColorTypeVal))
|
|
// {
|
|
// if (foreground.Count > 0)
|
|
// foreground.Pop();
|
|
// break;
|
|
// }
|
|
//
|
|
// if (eColorTypeVal == 0)
|
|
// {
|
|
// if (foreground.Count > 0)
|
|
// foreground.Pop();
|
|
// }
|
|
// else if (Sheets.UIColorSheet.TryGetRow(eColorTypeVal, out var row))
|
|
// {
|
|
// foreground.Push(row.Dark);
|
|
// }
|
|
// break;
|
|
// case MacroCode.EdgeColorType:
|
|
// if (!payload.TryGetExpression(out var eEdgeColor) || !eEdgeColor.TryGetUInt(out var eEdgeColorVal))
|
|
// {
|
|
// if (glow.Count > 0)
|
|
// glow.Pop();
|
|
// break;
|
|
// }
|
|
//
|
|
// if (eEdgeColorVal == 0)
|
|
// {
|
|
// if (glow.Count > 0)
|
|
// glow.Pop();
|
|
// }
|
|
// else if (Sheets.UIColorSheet.TryGetRow(eEdgeColorVal, out var row))
|
|
// {
|
|
// glow.Push(row.Dark);
|
|
// }
|
|
// break;
|
|
// case MacroCode.Fixed:
|
|
// if (!payload.TryGetExpression(out var expr1, out var expr2))
|
|
// break;
|
|
//
|
|
// if (expr1.TryGetUInt(out var group) && expr2.TryGetUInt(out var key))
|
|
// {
|
|
// chunks.Add(new IconChunk(source, null, BitmapFontIcon.AutoTranslateBegin));
|
|
// using var rssb = new RentedSeStringBuilder();
|
|
// var translatePayload = rssb.Builder
|
|
// .BeginMacro(MacroCode.Fixed)
|
|
// .AppendUIntExpression(group - 1)
|
|
// .AppendUIntExpression(key)
|
|
// .EndMacro()
|
|
// .ToReadOnlySeString();
|
|
//
|
|
// Append(Plugin.Evaluator.Evaluate(translatePayload).ToString());
|
|
// chunks.Add(new IconChunk(source, null, BitmapFontIcon.AutoTranslateEnd));
|
|
// }
|
|
// break;
|
|
// case MacroCode.Icon:
|
|
// if (payload.TryGetExpression(out var eIcon) && TryResolveInt(eIcon, out var iconVal))
|
|
// chunks.Add(new IconChunk(source, link, (BitmapFontIcon)iconVal));
|
|
// break;
|
|
// case MacroCode.Link:
|
|
// if (!payload.TryGetExpression(
|
|
// out var linkTypeExpr1,
|
|
// out var uintExpr2,
|
|
// out var intExpr3,
|
|
// out var intExpr4,
|
|
// out var strExpr5))
|
|
// break;
|
|
//
|
|
// if (!linkTypeExpr1.TryGetUInt(out var linkType))
|
|
// break;
|
|
//
|
|
// switch ((LinkMacroPayloadType)linkType)
|
|
// {
|
|
// case LinkMacroPayloadType.Terminator:
|
|
// link = null;
|
|
// break;
|
|
// case LinkMacroPayloadType.MapPosition:
|
|
// if (!uintExpr2.TryGetUInt(out var ids))
|
|
// break;
|
|
//
|
|
// if (!intExpr3.TryGetInt(out var rawX))
|
|
// break;
|
|
//
|
|
// if (!intExpr4.TryGetInt(out var rawY))
|
|
// break;
|
|
//
|
|
// var mapId = ids & 0xFF;
|
|
// var territoryId = (ids >> 16) & 0xFF;
|
|
// break;
|
|
// case (LinkMacroPayloadType)Payload.EmbeddedInfoType.DalamudLink - 1:
|
|
// if (!uintExpr2.TryGetUInt(out var commandId))
|
|
// break;
|
|
//
|
|
// if (!intExpr3.TryGetInt(out var extra1))
|
|
// break;
|
|
//
|
|
// if (!intExpr4.TryGetInt(out var extra2))
|
|
// break;
|
|
//
|
|
// if (!strExpr5.TryGetString(out var extraStr))
|
|
// break;
|
|
// break;
|
|
// case LinkMacroPayloadType.Quest:
|
|
// if (!uintExpr2.TryGetUInt(out var questId))
|
|
// break;
|
|
// break;
|
|
// case LinkMacroPayloadType.Status:
|
|
// if (!uintExpr2.TryGetUInt(out var statusId))
|
|
// break;
|
|
// break;
|
|
// case LinkMacroPayloadType.Item:
|
|
// if (!uintExpr2.TryGetUInt(out var itemId))
|
|
// break;
|
|
// break;
|
|
// case LinkMacroPayloadType.Character:
|
|
// if (!uintExpr2.TryGetUInt(out var flags))
|
|
// break;
|
|
//
|
|
// if (!intExpr3.TryGetUInt(out var worldId))
|
|
// break;
|
|
// break;
|
|
// case LinkMacroPayloadType.PartyFinder:
|
|
// if (!uintExpr2.TryGetUInt(out var listingId))
|
|
// break;
|
|
//
|
|
// // intExpr3 is unused
|
|
//
|
|
// if (!intExpr4.TryGetUInt(out worldId))
|
|
// break;
|
|
// break;
|
|
// case LinkMacroPayloadType.PartyFinderNotification:
|
|
// // no expr used
|
|
// break;
|
|
// case LinkMacroPayloadType.Achievement:
|
|
// if (!uintExpr2.TryGetUInt(out var achievementId))
|
|
// break;
|
|
// break;
|
|
// }
|
|
// break;
|
|
// case MacroCode.NonBreakingSpace:
|
|
// Append(" ");
|
|
// break;
|
|
// case PayloadType.Unknown:
|
|
// var rawPayload = (RawPayload)payload;
|
|
// else if (rawPayload.Data.Length > 1 && rawPayload.Data[1] == 0x14)
|
|
// {
|
|
// if (glow.Count > 0)
|
|
// {
|
|
// glow.Pop();
|
|
// }
|
|
// else if (rawPayload.Data.Length > 6 && rawPayload.Data[2] == 0x05 && rawPayload.Data[3] == 0xF6)
|
|
// {
|
|
// var (r, g, b) = (rawPayload.Data[4], rawPayload.Data[5], rawPayload.Data[6]);
|
|
// glow.Push(ColourUtil.ComponentsToRgba(r, g, b));
|
|
// }
|
|
// }
|
|
// break;
|
|
// }
|
|
// }
|
|
//
|
|
// return chunks;
|
|
// }
|
|
|
|
internal static IEnumerable<Chunk> ToChunks(SeString msg, ChunkSource source, ChatType? defaultColour)
|
|
{
|
|
var chunks = new List<Chunk>();
|
|
|
|
var italic = false;
|
|
var foreground = new Stack<uint>();
|
|
var glow = new Stack<uint>();
|
|
Payload? link = null;
|
|
|
|
void Append(string text)
|
|
{
|
|
chunks.Add(new TextChunk(source, link, text)
|
|
{
|
|
FallbackColour = defaultColour,
|
|
Foreground = foreground.Count > 0 ? foreground.Peek() : null,
|
|
Glow = glow.Count > 0 ? glow.Peek() : null,
|
|
Italic = italic,
|
|
});
|
|
}
|
|
|
|
foreach (var payload in msg.Payloads)
|
|
{
|
|
switch (payload.Type)
|
|
{
|
|
case PayloadType.EmphasisItalic:
|
|
var newStatus = ((EmphasisItalicPayload) payload).IsEnabled;
|
|
italic = newStatus;
|
|
break;
|
|
case PayloadType.UIForeground:
|
|
var foregroundPayload = (UIForegroundPayload) payload;
|
|
if (foregroundPayload.IsEnabled)
|
|
foreground.Push(foregroundPayload.UIColor.Value.Dark);
|
|
else if (foreground.Count > 0)
|
|
foreground.Pop();
|
|
break;
|
|
case PayloadType.UIGlow:
|
|
var glowPayload = (UIGlowPayload) payload;
|
|
if (glowPayload.IsEnabled)
|
|
glow.Push(glowPayload.UIColor.Value.Light);
|
|
else if (glow.Count > 0)
|
|
glow.Pop();
|
|
break;
|
|
case PayloadType.AutoTranslateText:
|
|
chunks.Add(new IconChunk(source, payload, BitmapFontIcon.AutoTranslateBegin));
|
|
var autoText = ((AutoTranslatePayload) payload).Text;
|
|
Append(autoText.Substring(2, autoText.Length - 4));
|
|
chunks.Add(new IconChunk(source, link, BitmapFontIcon.AutoTranslateEnd));
|
|
break;
|
|
case PayloadType.Icon:
|
|
chunks.Add(new IconChunk(source, link, ((IconPayload) payload).Icon));
|
|
break;
|
|
case PayloadType.MapLink:
|
|
case PayloadType.Quest:
|
|
case PayloadType.DalamudLink:
|
|
case PayloadType.Status:
|
|
case PayloadType.Item:
|
|
case PayloadType.Player:
|
|
link = payload;
|
|
break;
|
|
case PayloadType.PartyFinder:
|
|
link = payload;
|
|
break;
|
|
case PayloadType.Unknown:
|
|
var rawPayload = (RawPayload) payload;
|
|
var colorPayload = ColorPayload.From(rawPayload.Data);
|
|
if (colorPayload != null)
|
|
{
|
|
if (colorPayload.Enabled)
|
|
{
|
|
if (colorPayload.Color > 0)
|
|
foreground.Push(colorPayload.Color);
|
|
else if (foreground.Count > 0) // Push the previous color as we don't want invisible text
|
|
foreground.Push(foreground.Peek());
|
|
}
|
|
else if (foreground.Count > 0)
|
|
{
|
|
foreground.Pop();
|
|
}
|
|
}
|
|
else if (rawPayload.Data.Length > 1 && rawPayload.Data[1] == 0x14)
|
|
{
|
|
if (glow.Count > 0)
|
|
{
|
|
glow.Pop();
|
|
}
|
|
else if (rawPayload.Data.Length > 6 && rawPayload.Data[2] == 0x05 && rawPayload.Data[3] == 0xF6)
|
|
{
|
|
var (r, g, b) = (rawPayload.Data[4], rawPayload.Data[5], rawPayload.Data[6]);
|
|
glow.Push(ColourUtil.ComponentsToRgba(r, g, b));
|
|
}
|
|
}
|
|
else if (rawPayload.Data.Length > 7 && rawPayload.Data[1] == 0x27 && rawPayload.Data[3] == 0x0A)
|
|
{
|
|
// pf payload
|
|
var reader = new BinaryReader(new MemoryStream(rawPayload.Data[4..]));
|
|
var id = GetInteger(reader);
|
|
link = new PartyFinderPayload(id);
|
|
}
|
|
else if (rawPayload.Data.Length > 5 && rawPayload.Data[1] == 0x27 && rawPayload.Data[3] == 0x06)
|
|
{
|
|
// achievement payload
|
|
var reader = new BinaryReader(new MemoryStream(rawPayload.Data[4..]));
|
|
var id = GetInteger(reader);
|
|
link = new AchievementPayload(id);
|
|
}
|
|
else if (rawPayload.Data is [_, (byte)MacroCode.NonBreakingSpace, _, _])
|
|
{
|
|
// NonBreakingSpace payload
|
|
Append(" ");
|
|
}
|
|
// NOTE: no URIPayload because it originates solely from
|
|
// new Message(). The game doesn't have a URI payload type.
|
|
else if (Equals(rawPayload, RawPayload.LinkTerminator))
|
|
{
|
|
link = null;
|
|
}
|
|
break;
|
|
default:
|
|
if (payload is ITextProvider textProvider)
|
|
{
|
|
// We don't want to parse any null string
|
|
var str = textProvider.Text;
|
|
var nulIndex = str.IndexOf('\0');
|
|
if (nulIndex > 0)
|
|
str = str[..nulIndex];
|
|
if (string.IsNullOrEmpty(str))
|
|
break;
|
|
|
|
Append(str);
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
|
|
return chunks;
|
|
}
|
|
|
|
internal static string ToRawString(List<Chunk> chunks)
|
|
{
|
|
if (chunks.Count == 0)
|
|
return string.Empty;
|
|
|
|
var builder = new StringBuilder();
|
|
foreach (var chunk in chunks)
|
|
if (chunk is TextChunk text)
|
|
builder.Append(text.Content);
|
|
|
|
return builder.ToString();
|
|
}
|
|
|
|
// Hellion Chat — shared helper for Auto-Tell-Tabs and the MessageStore
|
|
// history-preload query. Walks the chunk list once and returns the
|
|
// first PlayerPayload it finds, or null when the message has no
|
|
// resolved player link (e.g. system messages, GM tells we already
|
|
// skipped earlier in the pipeline).
|
|
internal static PlayerPayload? TryGetPlayerPayload(IReadOnlyList<Chunk> chunks)
|
|
{
|
|
foreach (var chunk in chunks)
|
|
{
|
|
if (chunk.Link is PlayerPayload pp)
|
|
{
|
|
return pp;
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
// Fallback for tells where the PlayerPayload lives in the raw SeString
|
|
// payload list rather than on a chunk's Link slot. Same semantics as
|
|
// the chunk-walking variant above: returns the first PlayerPayload or
|
|
// null if the SeString has none.
|
|
internal static PlayerPayload? TryGetPlayerPayload(SeString? seString)
|
|
{
|
|
if (seString == null)
|
|
{
|
|
return null;
|
|
}
|
|
foreach (var payload in seString.Payloads)
|
|
{
|
|
if (payload is PlayerPayload pp)
|
|
{
|
|
return pp;
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
// True when the message's sender (or, as a fallback, content) carries a
|
|
// PlayerPayload that matches the given identity. Used by both the
|
|
// Tab.Matches sender filter and the MessageStore tell-history scan.
|
|
internal static bool MatchesSender(Message message, string senderName, uint senderWorld)
|
|
{
|
|
var payload = TryGetPlayerPayload(message.Sender) ?? TryGetPlayerPayload(message.Content);
|
|
if (payload == null)
|
|
{
|
|
return false;
|
|
}
|
|
if (!string.Equals(payload.PlayerName, senderName, StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
return false;
|
|
}
|
|
return payload.World.RowId == senderWorld;
|
|
}
|
|
|
|
internal static readonly RawPayload PeriodicRecruitmentLink = new([0x02, 0x27, 0x07, 0x08, 0x01, 0x01, 0x01, 0xFF, 0x01, 0x03]);
|
|
|
|
private static uint GetInteger(BinaryReader input)
|
|
{
|
|
var num1 = (uint) input.ReadByte();
|
|
if (num1 < 208U)
|
|
return num1 - 1U;
|
|
|
|
var num2 = (uint) ((int) num1 + 1 & 15);
|
|
var numArray = new byte[4];
|
|
for (var index = 3; index >= 0; --index)
|
|
numArray[index] = (num2 & 1 << index) == 0L ? (byte) 0 : input.ReadByte();
|
|
|
|
return BitConverter.ToUInt32(numArray, 0);
|
|
}
|
|
|
|
// private static bool TryResolveUInt(in ReadOnlySeExpressionSpan expression, out uint value)
|
|
// {
|
|
// if (expression.TryGetUInt(out value))
|
|
// return true;
|
|
//
|
|
// if (expression.TryGetParameterExpression(out var exprType, out var operand1))
|
|
// {
|
|
// if (!TryResolveUInt(operand1, out var paramIndex))
|
|
// return false;
|
|
//
|
|
// if (paramIndex == 0)
|
|
// return false;
|
|
//
|
|
// paramIndex--;
|
|
// if ((ExpressionType)exprType == ExpressionType.GlobalNumber)
|
|
// {
|
|
// value = (uint) GlobalParametersCache.GetValue((int)paramIndex);
|
|
// return true;
|
|
// }
|
|
// // return (ExpressionType)exprType switch
|
|
// // {
|
|
// // // ExpressionType.LocalNumber => context.TryGetLNum((int)paramIndex, out value), // lnum
|
|
// // ExpressionType.GlobalNumber => (uint) GlobalParametersCache.GetValue((int)paramIndex), // gnum
|
|
// // _ => false, // gstr, lstr
|
|
// // };
|
|
// }
|
|
//
|
|
// return false;
|
|
// }
|
|
|
|
// [MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
// private static bool TryResolveInt(in ReadOnlySeExpressionSpan expression, out int value)
|
|
// {
|
|
// if (TryResolveUInt(expression, out var u32))
|
|
// {
|
|
// value = (int)u32;
|
|
// return true;
|
|
// }
|
|
//
|
|
// value = 0;
|
|
// return false;
|
|
// }
|
|
}
|