using Dalamud.Bindings.ImGui; using Dalamud.Game.Text.SeStringHandling; using Dalamud.Game.Text.SeStringHandling.Payloads; using Dalamud.Interface; using Dalamud.Interface.ImGuiSeStringRenderer; using Dalamud.Interface.Utility; using Lumina.Text; using System; using System.Numerics; using System.Threading; using DalamudSeString = Dalamud.Game.Text.SeStringHandling.SeString; using DalamudSeStringBuilder = Dalamud.Game.Text.SeStringHandling.SeStringBuilder; using LuminaSeStringBuilder = Lumina.Text.SeStringBuilder; namespace LightlessSync.Utils; public static class SeStringUtils { private static int _seStringHitboxCounter; private static int _iconHitboxCounter; public static DalamudSeString BuildFormattedPlayerName(string text, Vector4? textColor, Vector4? glowColor) { var b = new DalamudSeStringBuilder(); if (glowColor is Vector4 glow) b.Add(new GlowPayload(glow)); if (textColor is Vector4 color) b.Add(new ColorPayload(color)); b.AddText(text ?? string.Empty); if (textColor is not null) b.Add(new ColorEndPayload()); if (glowColor is not null) b.Add(new GlowEndPayload()); return b.Build(); } public static DalamudSeString BuildPlain(string text) { var b = new DalamudSeStringBuilder(); b.AddText(text ?? string.Empty); return b.Build(); } public static DalamudSeString BuildRichText(ReadOnlySpan fragments) { var builder = new LuminaSeStringBuilder(); foreach (var fragment in fragments) { if (string.IsNullOrEmpty(fragment.Text)) continue; var hasColor = fragment.Color.HasValue; if (hasColor) { Vector4 color = fragment.Color!.Value; builder.PushColorRgba(color); } if (fragment.Bold) builder.AppendSetBold(true); builder.Append(fragment.Text.AsSpan()); if (fragment.Bold) builder.AppendSetBold(false); if (hasColor) builder.PopColor(); } return DalamudSeString.Parse(builder.ToArray()); } public static DalamudSeString BuildRichText(params RichTextEntry[] fragments) => BuildRichText(fragments.AsSpan()); public static void RenderSeString(DalamudSeString seString, Vector2 position, ImFontPtr? font = null, ImDrawListPtr? drawList = null) { drawList ??= ImGui.GetWindowDrawList(); var drawParams = new SeStringDrawParams { Font = font ?? UiBuilder.MonoFont, Color = 0xFFFFFFFF, WrapWidth = float.MaxValue, TargetDrawList = drawList }; ImGui.SetCursorScreenPos(position); ImGuiHelpers.SeStringWrapped(seString.Encode(), drawParams); var textSize = ImGui.CalcTextSize(seString.TextValue); if (textSize.Y <= 0f) textSize.Y = ImGui.GetTextLineHeight(); ImGui.Dummy(new Vector2(0f, textSize.Y)); } public static void RenderSeStringWrapped(DalamudSeString seString, float wrapWidth, ImFontPtr? font = null, ImDrawListPtr? drawList = null) { drawList ??= ImGui.GetWindowDrawList(); var drawParams = new SeStringDrawParams { Font = font ?? ImGui.GetFont(), Color = ImGui.GetColorU32(ImGuiCol.Text), WrapWidth = wrapWidth, TargetDrawList = drawList }; ImGuiHelpers.SeStringWrapped(seString.Encode(), drawParams); var calcWrapWidth = wrapWidth > 0f ? wrapWidth : -1f; var textSize = ImGui.CalcTextSize(seString.TextValue, wrapWidth: calcWrapWidth); if (textSize.Y <= 0f) textSize.Y = ImGui.GetTextLineHeight(); ImGui.Dummy(new Vector2(0f, textSize.Y)); } public static Vector2 RenderSeStringWithHitbox(DalamudSeString seString, Vector2 position, ImFontPtr? font = null, string? id = null) { var drawList = ImGui.GetWindowDrawList(); var drawParams = new SeStringDrawParams { Font = font ?? UiBuilder.MonoFont, Color = 0xFFFFFFFF, WrapWidth = float.MaxValue, TargetDrawList = drawList }; ImGui.SetCursorScreenPos(position); ImGuiHelpers.SeStringWrapped(seString.Encode(), drawParams); var textSize = ImGui.CalcTextSize(seString.TextValue); ImGui.SetCursorScreenPos(position); if (id is not null) { ImGui.PushID(id); } else { ImGui.PushID(Interlocked.Increment(ref _seStringHitboxCounter)); } try { ImGui.InvisibleButton("##hitbox", textSize); } finally { ImGui.PopID(); } return textSize; } public static Vector2 RenderIconWithHitbox(int iconId, Vector2 position, ImFontPtr? font = null, string? id = null) { var drawList = ImGui.GetWindowDrawList(); var drawParams = new SeStringDrawParams { Font = font ?? UiBuilder.MonoFont, Color = 0xFFFFFFFF, WrapWidth = float.MaxValue, TargetDrawList = drawList }; var iconMacro = $""; var drawResult = ImGuiHelpers.CompileSeStringWrapped(iconMacro, drawParams); ImGui.SetCursorScreenPos(position); if (id is not null) { ImGui.PushID(id); } else { ImGui.PushID(Interlocked.Increment(ref _iconHitboxCounter)); } try { ImGui.InvisibleButton("##iconHitbox", drawResult.Size); } finally { ImGui.PopID(); } return drawResult.Size; } #region Internal Payloads public readonly record struct RichTextEntry(string Text, Vector4? Color = null, bool Bold = false); private abstract class AbstractColorPayload : Payload { protected byte Red { get; init; } protected byte Green { get; init; } protected byte Blue { get; init; } protected override byte[] EncodeImpl() { return new byte[] { 0x02, ChunkType, 0x05, 0xF6, Red, Green, Blue, 0x03 }; } protected override void DecodeImpl(BinaryReader reader, long endOfStream) { } public override PayloadType Type => PayloadType.Unknown; protected abstract byte ChunkType { get; } } private abstract class AbstractColorEndPayload : Payload { protected override byte[] EncodeImpl() { return new byte[] { 0x02, ChunkType, 0x02, 0xEC, 0x03 }; } protected override void DecodeImpl(BinaryReader reader, long endOfStream) { } public override PayloadType Type => PayloadType.Unknown; protected abstract byte ChunkType { get; } } private sealed class ColorPayload : AbstractColorPayload { protected override byte ChunkType => 0x13; public ColorPayload(Vector3 color) { Red = Math.Max((byte)1, (byte)(color.X * 255f)); Green = Math.Max((byte)1, (byte)(color.Y * 255f)); Blue = Math.Max((byte)1, (byte)(color.Z * 255f)); } public ColorPayload(Vector4 color) : this(new Vector3(color.X, color.Y, color.Z)) { } } private sealed class ColorEndPayload : AbstractColorEndPayload { protected override byte ChunkType => 0x13; } private sealed class GlowPayload : AbstractColorPayload { protected override byte ChunkType => 0x14; public GlowPayload(Vector3 color) { Red = Math.Max((byte)1, (byte)(color.X * 255f)); Green = Math.Max((byte)1, (byte)(color.Y * 255f)); Blue = Math.Max((byte)1, (byte)(color.Z * 255f)); } public GlowPayload(Vector4 color) : this(new Vector3(color.X, color.Y, color.Z)) { } } private sealed class GlowEndPayload : AbstractColorEndPayload { protected override byte ChunkType => 0x14; } #endregion }