- Fully support role colors and icons

- Fix #5
- Add SeString debugger window
This commit is contained in:
Infi
2024-04-09 15:58:53 +02:00
parent fed420901c
commit 5da271e0c2
10 changed files with 611 additions and 117 deletions
+1 -3
View File
@@ -102,9 +102,7 @@ public sealed class PayloadHandler {
}
var contentId = chunk.Message?.ContentId ?? 0;
var sender = chunk.Message?.Sender
.Select(chunk => chunk.Link)
.FirstOrDefault(chunk => chunk is PlayerPayload) as PlayerPayload;
var sender = chunk.Message?.Sender.Select(c => c.Link).FirstOrDefault(p => p is PlayerPayload) as PlayerPayload;
if (ImGui.BeginMenu(Language.Context_Integrations)) {
var cursor = ImGui.GetCursorPos();
+4
View File
@@ -42,6 +42,7 @@ public sealed class Plugin : IDalamudPlugin {
public SettingsWindow SettingsWindow { get; }
public ChatLogWindow ChatLogWindow { get; }
public CommandHelpWindow CommandHelpWindow { get; }
public SeStringDebugger SeStringDebugger { get; }
internal Configuration Config { get; }
internal Commands Commands { get; }
@@ -81,10 +82,12 @@ public sealed class Plugin : IDalamudPlugin {
ChatLogWindow = new ChatLogWindow(this);
SettingsWindow = new SettingsWindow(this);
CommandHelpWindow = new CommandHelpWindow(ChatLogWindow);
SeStringDebugger = new SeStringDebugger(this);
WindowSystem.AddWindow(ChatLogWindow);
WindowSystem.AddWindow(SettingsWindow);
WindowSystem.AddWindow(CommandHelpWindow);
WindowSystem.AddWindow(SeStringDebugger);
FontManager.BuildFonts();
Interface.UiBuilder.DisableCutsceneUiHide = true;
@@ -114,6 +117,7 @@ public sealed class Plugin : IDalamudPlugin {
WindowSystem.RemoveAllWindows();
ChatLogWindow.Dispose();
SettingsWindow.Dispose();
SeStringDebugger.Dispose();
ExtraChat.Dispose();
Ipc.Dispose();
+2
View File
@@ -257,6 +257,7 @@ internal class Store : IDisposable {
}
}
public (SeString? Sender, SeString? Message) LastMessage = (null, null);
private void ChatMessage(XivChatType type, uint senderId, SeString sender, SeString message) {
var chatCode = new ChatCode((ushort) type);
@@ -265,6 +266,7 @@ internal class Store : IDisposable {
formatting = FormatFor(chatCode.Type);
}
LastMessage = (sender, message);
var senderChunks = new List<Chunk>();
if (formatting is { IsPresent: true }) {
senderChunks.Add(new TextChunk(ChunkSource.None, null, formatting.Before) {
+8 -14
View File
@@ -75,12 +75,8 @@ public sealed class ChatLogWindow : Window, IUiComponent {
Plugin = plugin;
Salt = new Random().Next().ToString();
Size = new Vector2(500, 250);
SizeCondition = ImGuiCond.FirstUseEver;
SizeConstraints = new WindowSizeConstraints
{
MinimumSize = new Vector2(500, 250),
MaximumSize = new Vector2(float.MaxValue, float.MaxValue)
};
PayloadHandler = new PayloadHandler(this);
HandlerLender = new Lender<PayloadHandler>(() => new PayloadHandler(this));
@@ -1257,10 +1253,8 @@ public sealed class ChatLogWindow : Window, IUiComponent {
|| cmd.Alias.RawString == command
|| cmd.ShortCommand.RawString == command
|| cmd.ShortAlias.RawString == command);
if (cmd != null) {
if (cmd != null)
Plugin.CommandHelpWindow.UpdateContent(cmd);
Plugin.CommandHelpWindow.IsOpen = true;
}
}
if (data->EventFlag != ImGuiInputTextFlags.CallbackHistory) {
@@ -1332,15 +1326,15 @@ public sealed class ChatLogWindow : Window, IUiComponent {
private void DrawChunk(Chunk chunk, bool wrap = true, PayloadHandler? handler = null, float lineWidth = 0f) {
if (chunk is IconChunk icon && _fontIcon != null) {
var bounds = IconUtil.GetBounds((byte) icon.Icon);
if (bounds != null) {
var bounds = IconUtil.GfdFileView.TryGetEntry((uint) icon.Icon, out var entry);
if (bounds) {
var texSize = new Vector2(_fontIcon.Width, _fontIcon.Height);
var sizeRatio = Plugin.Config.FontSize / bounds.Value.W;
var size = new Vector2(bounds.Value.Z, bounds.Value.W) * sizeRatio * ImGuiHelpers.GlobalScale;
var sizeRatio = Plugin.Config.FontSize / entry.Height;
var size = new Vector2(entry.Width, entry.Height) * sizeRatio * ImGuiHelpers.GlobalScale;
var uv0 = new Vector2(bounds.Value.X, bounds.Value.Y - 2) / texSize;
var uv1 = new Vector2(bounds.Value.X + bounds.Value.Z, bounds.Value.Y - 2 + bounds.Value.W) / texSize;
var uv0 = new Vector2(entry.Left, entry.Top) / texSize;
var uv1 = new Vector2(entry.Left + entry.Width, entry.Top + entry.Height) / texSize;
ImGui.Image(_fontIcon.ImGuiHandle, size, uv0, uv1);
ImGuiUtil.PostPayload(chunk, handler);
}
+4
View File
@@ -20,6 +20,7 @@ public class CommandHelpWindow : Window {
ImGuiWindowFlags.NoResize | ImGuiWindowFlags.NoFocusOnAppearing | ImGuiWindowFlags.AlwaysAutoResize;
}
// Sets IsOpen to true if it should be drawn
public void UpdateContent(TextCommand command)
{
Command = command;
@@ -36,6 +37,7 @@ public class CommandHelpWindow : Window {
break;
case CommandHelpSide.None:
default:
IsOpen = false;
return;
}
@@ -45,6 +47,8 @@ public class CommandHelpWindow : Window {
MinimumSize = new Vector2(width, 0),
MaximumSize = LogWindow.LastWindowSize with { X = width }
};
IsOpen = true;
}
public override void Draw()
+319
View File
@@ -0,0 +1,319 @@
using System.Numerics;
using System.Text;
using ChatTwo.Util;
using Dalamud.Game.Text.SeStringHandling.Payloads;
using Dalamud.Interface.Windowing;
using ImGuiNET;
using DalamudPartyFinderPayload = Dalamud.Game.Text.SeStringHandling.Payloads.PartyFinderPayload;
namespace ChatTwo.Ui;
public class SeStringDebugger : Window
{
private readonly Plugin Plugin;
public SeStringDebugger(Plugin plugin) : base($"SeString Debugger###chat2-sestringdebugger")
{
Plugin = plugin;
SizeConstraints = new WindowSizeConstraints
{
MinimumSize = new Vector2(475, 600),
MaximumSize = new Vector2(float.MaxValue, float.MaxValue)
};
Plugin.Commands.Register("/chat2Debugger").Execute += Toggle;
}
public void Dispose()
{
Plugin.Commands.Register("/chat2Debugger").Execute -= Toggle;
}
private void Toggle(string _, string __) => Toggle();
public override void Draw()
{
ImGui.TextUnformatted("SeString Content");
ImGui.Spacing();
if (Plugin.Store.LastMessage.Sender == null)
{
ImGui.TextUnformatted("Nothing to show");
return;
}
// TODO: Make SeString freely selectable through chat
foreach (var payload in Plugin.Store.LastMessage.Sender.Payloads)
{
switch (payload)
{
case UIForegroundPayload color:
{
RenderMetadataDictionary("Link ForegroundColor", new Dictionary<string, string?>
{
{ "Enabled?", color.IsEnabled.ToString() },
{ "ColorKey", color.IsEnabled ? color.ColorKey.ToString() : "Color Ended" },
});
break;
}
case MapLinkPayload map:
{
RenderMetadataDictionary("Link MapLinkPayload", new Dictionary<string, string?>
{
{ "Map.RowId", map.Map?.RowId.ToString() },
{ "Map.PlaceName", map.Map?.PlaceName.Value?.Name.ToString() },
{ "Map.PlaceNameRegion", map.Map?.PlaceNameRegion.Value?.Name.ToString() },
{ "Map.PlaceNameSub", map.Map?.PlaceNameSub.Value?.Name.ToString() },
{ "TerritoryType.RowId", map.TerritoryType?.RowId.ToString() },
{ "RawX", map.RawX.ToString() },
{ "RawY", map.RawY.ToString() },
{ "XCoord", map.XCoord.ToString() },
{ "YCoord", map.YCoord.ToString() },
{ "CoordinateString", map.CoordinateString },
{ "DataString", map.DataString },
});
break;
}
case QuestPayload quest:
{
RenderMetadataDictionary("Link QuestPayload", new Dictionary<string, string?>
{
{ "Quest.RowId", quest.Quest?.RowId.ToString() },
{ "Quest.Name", quest.Quest?.Name.ToString() },
});
break;
}
case DalamudLinkPayload link:
{
RenderMetadataDictionary("Link DalamudLinkPayload", new Dictionary<string, string?>
{
{ "CommandId", link.CommandId.ToString() },
{ "Plugin", link.Plugin },
});
break;
}
case DalamudPartyFinderPayload pf:
{
RenderMetadataDictionary("Link PartyFinderPayload", new Dictionary<string, string?>
{
{ "ListingId", pf.ListingId.ToString() },
{ "LinkType", EnumName(pf.LinkType) },
});
break;
}
case PlayerPayload player:
{
RenderMetadataDictionary("Link PlayerPayload", new Dictionary<string, string?>
{
{ "Real", player.DisplayedName },
{ "PlayerName", player.PlayerName },
{ "World.Name", player.World.Name },
});
break;
}
case ItemPayload item:
{
RenderMetadataDictionary("Link ItemPayload", new Dictionary<string, string?>
{
{ "ItemId", item.ItemId.ToString() },
{ "RawItemId", item.RawItemId.ToString() },
{ "Kind", EnumName(item.Kind) },
{ "IsHQ", item.IsHQ.ToString() },
{ "Item.Name", item.Item?.Name.ToString() },
});
break;
}
case AutoTranslatePayload at:
{
RenderMetadataDictionary("Link AutoTranslatePayload", new Dictionary<string, string?>
{
{ "Text", at.Text },
});
break;
}
case IconPayload icon:
{
var found = IconUtil.GfdFileView.TryGetEntry((uint) icon.Icon, out var entry);
RenderMetadataDictionary("Link IconPayload", new Dictionary<string, string?>
{
{ "Found", found.ToString() },
{ "Icon ID", ((uint) icon.Icon).ToString() },
});
break;
}
case RawPayload raw:
{
var colorPayload = ColorPayload.From(raw.Data);
if (colorPayload != null)
{
var push = colorPayload.Enabled && colorPayload.Color != 0;
// if (push) ImGui.PushStyleColor(ImGuiCol.Text, ColourUtil.RgbaToAbgr(colorPayload.U));
RenderMetadataDictionary("Link ColorPayload", new Dictionary<string, string?>
{
{ "Unshifted", colorPayload.UnshiftedColor.ToString("X8") },
{ "Color", colorPayload.Color.ToString("X8") },
{ "Enabled?", colorPayload.Enabled.ToString() },
});
// if (push) ImGui.PopStyleColor();
}
else
{
RenderMetadataDictionary("Link RawPayload", new Dictionary<string, string?>
{
{ "Data", string.Join(" ", raw.Data.Select(b => b.ToString("X2"))) },
{ "Type", EnumName(raw.Type) },
});
}
break;
}
case StatusPayload status:
{
RenderMetadataDictionary("Link StatusPayload", new Dictionary<string, string?>
{
{ "Status.RowId", status.Status.RowId.ToString() },
{ "Status.Name", status.Status.Name },
{ "Status.Icon", status.Status.Icon.ToString() }
});
break;
}
case Util.PartyFinderPayload pf:
{
RenderMetadataDictionary("Link PartyFinderPayload", new Dictionary<string, string?>
{
{ "Id", pf.Id.ToString() }
});
break;
}
case AchievementPayload achievement:
{
RenderMetadataDictionary("Link AchievementPayload", new Dictionary<string, string?>
{
{ "Id", achievement.Id.ToString() }
});
break;
}
default:
var payloadData = payload.Encode();
var initialByte = payloadData.First();
if (initialByte != 0x02)
{
RenderMetadataDictionary("Text Payload", new Dictionary<string, string?>
{
{ "Content", Encoding.UTF8.GetString(payloadData) },
});
}
else
{
var unknown = new RawPayload(payloadData);
RenderMetadataDictionary("Link Unknown", new Dictionary<string, string?>
{
{ "Unknown", string.Join(" ", unknown.Data.Select(b => b.ToString("X2"))) },
});
}
break;
}
}
}
private static string? EnumName<T>(T? value) where T : Enum
{
if (value == null)
{
return null;
}
var rawValue = Convert.ChangeType(value, value.GetTypeCode());
return (Enum.GetName(value.GetType(), value) ?? "Unknown") + $" ({rawValue})";
}
private static void RenderMetadataDictionary(string name, Dictionary<string, string?> metadata)
{
var style = ImGui.GetStyle();
ImGui.Text($"{name}:");
ImGui.Indent(style.IndentSpacing);
if (!ImGui.BeginTable($"##chat3-{name}", 2, 0))
{
ImGui.EndTable();
ImGui.Unindent(style.IndentSpacing);
return;
}
ImGui.TableSetupColumn($"##chat3-{name}-key", 0, 0.4f);
ImGui.TableSetupColumn($"##chat3-{name}-value");
for (var i = 0; i < metadata.Count; i++)
{
var (key, value) = metadata.ElementAt(i);
ImGui.PushID(i);
ImGui.TableNextColumn();
ImGui.Text(key);
ImGui.TableNextColumn();
ImGuiTextVisibleWhitespace(value);
ImGui.PopID();
}
ImGui.EndTable();
ImGui.Unindent(style.IndentSpacing);
ImGui.NewLine();
}
// ImGuiTextVisibleWhitespace replaces leading and trailing whitespace with
// visible characters. The extra characters are rendered with a muted font.
private static void ImGuiTextVisibleWhitespace(string? original, bool wrap = true)
{
if (string.IsNullOrEmpty(original))
{
var str = original == null ? "(null)" : "(empty)";
ImGui.PushStyleColor(ImGuiCol.Text, new Vector4(1, 1, 1, 0.5f));
ImGui.TextUnformatted(str);
ImGui.PopStyleColor();
return;
}
var text = original;
var start = 0;
var end = text.Length;
ImGui.PushStyleVar(ImGuiStyleVar.ItemSpacing, new Vector2(0, 0));
void WriteText(string text)
{
if (wrap)
{
ImGui.TextWrapped(text);
}
else
{
ImGui.TextUnformatted(text);
}
}
while (start < end && char.IsWhiteSpace(text[start]))
{
start++;
}
if (start > 0)
{
ImGui.PushStyleColor(ImGuiCol.Text, new Vector4(1, 1, 1, 0.5f));
WriteText(new string('_', start));
ImGui.PopStyleColor();
ImGui.SameLine();
}
while (end > start && char.IsWhiteSpace(text[end - 1]))
{
end--;
}
WriteText(text[start..end]);
if (end < text.Length)
{
ImGui.SameLine();
ImGui.PushStyleColor(ImGuiCol.Text, new Vector4(1, 1, 1, 0.5f));
WriteText(new string('_', text.Length - end));
ImGui.PopStyleColor();
}
ImGui.PopStyleVar();
}
}
+10 -6
View File
@@ -68,15 +68,19 @@ internal static class ChunkUtil {
break;
case PayloadType.Unknown:
var rawPayload = (RawPayload) payload;
if (rawPayload.Data.Length > 1 && rawPayload.Data[1] == 0x13)
var colorPayload = ColorPayload.From(rawPayload.Data);
if (colorPayload != null)
{
if (foreground.Count > 0) {
foreground.Pop();
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 (rawPayload.Data.Length > 6 && rawPayload.Data[2] == 0x05 && rawPayload.Data[3] == 0xF6)
else if (foreground.Count > 0)
{
var (r, g, b) = (rawPayload.Data[4], rawPayload.Data[5], rawPayload.Data[6]);
foreground.Push(ColourUtil.ComponentsToRgba(r, g, b));
foreground.Pop();
}
}
else if (rawPayload.Data.Length > 1 && rawPayload.Data[1] == 0x14)
+9 -2
View File
@@ -12,8 +12,8 @@ internal static class ColourUtil {
}
internal static uint RgbaToAbgr(uint rgba) {
var (r, g, b, a) = RgbaToComponents(rgba);
return (uint) ((a << 24) | (b << 16) | (g << 8) | r);
var tmp = ((rgba << 8) & 0xFF00FF00) | ((rgba >> 8) & 0xFF00FF);
return (tmp << 16) | (tmp >> 16);
}
internal static Vector3 RgbaToVector3(uint rgba) {
@@ -38,6 +38,13 @@ internal static class ColourUtil {
));
}
public static unsafe uint ArgbToRgba(uint x)
{
var buf = (byte*)&x;
(buf[1], buf[2], buf[3], buf[0]) = (buf[0], buf[1], buf[2], buf[3]);
return x;
}
internal static uint ComponentsToRgba(byte red, byte green, byte blue, byte alpha = 0xFF) => alpha
| (uint) (red << 24)
| (uint) (green << 16)
+109
View File
@@ -0,0 +1,109 @@
using Dalamud.Utility;
using FFXIVClientStructs.FFXIV.Client.UI.Misc;
using FFXIVClientStructs.FFXIV.Component.Text;
namespace ChatTwo.Util;
public class ColorPayload
{
private const byte START_BYTE = 2;
public bool Enabled;
public uint Color;
public uint UnshiftedColor;
public static ColorPayload? From(byte[] data)
{
using var stream = new MemoryStream(data);
if (stream.ReadByte() != START_BYTE || stream.ReadByte() != 0x13)
return null;
stream.ReadByte(); // skip the length byte;
var typeByte = stream.ReadByte();
var payload = new ColorPayload();
if (typeByte == 0xEC)
{
payload.Enabled = false;
return payload;
}
if (typeByte == 0xE9)
{
var param = stream.ReadByte();
var ok = TryGetGNumDefault((uint) (param - 2), out var value);
if (!ok)
{
Plugin.Log.Error($"Unable to GetGNum for param {param - 2}");
return null;
}
payload.Enabled = true;
payload.UnshiftedColor = value;
payload.Color = ColourUtil.ArgbToRgba(value);
return payload;
}
if (typeByte is >= 0xF0 and <= 0xFE)
{
// From: https://github.com/NotAdam/Lumina/blob/master/src/Lumina/Text/Expressions/IntegerExpression.cs#L119-L128
uint ShiftAndThrowIfZero(int v, int shift)
{
return v switch
{
-1 => throw new ArgumentException("Encountered premature end of input (unexpected EOF).", nameof(v)),
0 => throw new ArgumentException("Encountered premature end of input (unexpected null character).", nameof(v)),
_ => (uint)v << shift
};
}
typeByte += 1;
var value = 0u;
if ((typeByte & 8) != 0)
value |= ShiftAndThrowIfZero(stream.ReadByte(), 24);
else
value |= 0xff000000u;
if( (typeByte & 4) != 0 ) value |= ShiftAndThrowIfZero( stream.ReadByte(), 16 );
if( (typeByte & 2) != 0 ) value |= ShiftAndThrowIfZero( stream.ReadByte(), 8 );
if( (typeByte & 1) != 0 ) value |= ShiftAndThrowIfZero( stream.ReadByte(), 0 );
payload.Enabled = true;
payload.Color = ColourUtil.ArgbToRgba(value);
return payload;
}
return null;
}
private static unsafe bool TryGetGNumDefault(uint parameterIndex, out uint value)
{
value = 0u;
var rtm = RaptureTextModule.Instance();
if (rtm is null)
return false;
if (!ThreadSafety.IsMainThread)
{
Plugin.Log.Error("Global parameters may only be used from the main thread.");
return false;
}
ref var gp = ref rtm->TextModule.MacroDecoder.GlobalParameters;
if (parameterIndex >= gp.MySize)
return false;
var p = rtm->TextModule.MacroDecoder.GlobalParameters.Get(parameterIndex);
switch (p.Type)
{
case TextParameterType.Integer:
value = (uint)p.IntValue;
return true;
default:
return false;
}
}
}
+144 -91
View File
@@ -1,95 +1,148 @@
using System.Numerics;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
namespace ChatTwo.Util;
internal static class IconUtil {
internal static Vector4? GetBounds(byte id) => id switch {
1 => new Vector4(0, 342, 40, 40),
2 => new Vector4(40, 342, 40, 40),
3 => new Vector4(80, 342, 40, 40),
4 => new Vector4(120, 342, 40, 40),
5 => new Vector4(160, 342, 40, 40),
6 => new Vector4(0, 382, 40, 40),
7 => new Vector4(40, 382, 40, 40),
8 => new Vector4(80, 382, 40, 40),
9 => new Vector4(120, 382, 40, 40),
10 => new Vector4(160, 382, 40, 40),
11 => new Vector4(0, 422, 40, 40),
12 => new Vector4(40, 422, 40, 40),
13 => new Vector4(80, 422, 40, 40),
14 => new Vector4(120, 422, 40, 40),
15 => new Vector4(160, 422, 40, 40),
16 => new Vector4(120, 542, 40, 40),
17 => new Vector4(160, 542, 40, 40),
18 => new Vector4(0, 462, 108, 40),
19 => new Vector4(108, 462, 108, 40),
20 => new Vector4(120, 502, 40, 40),
21 => new Vector4(0, 502, 56, 40),
22 => new Vector4(56, 502, 64, 40),
23 => new Vector4(160, 502, 40, 40),
24 => new Vector4(0, 542, 56, 40),
25 => new Vector4(56, 542, 64, 40),
51 => new Vector4(248, 342, 40, 40),
52 => new Vector4(288, 342, 40, 40),
53 => new Vector4(328, 342, 40, 40),
54 => new Vector4(200, 342, 24, 40),
55 => new Vector4(224, 342, 24, 40),
56 => new Vector4(200, 382, 40, 40),
57 => new Vector4(240, 382, 40, 40),
58 => new Vector4(280, 382, 40, 40),
59 => new Vector4(200, 422, 40, 40),
60 => new Vector4(240, 422, 40, 40),
61 => new Vector4(280, 422, 40, 40),
62 => new Vector4(320, 382, 40, 40),
63 => new Vector4(320, 422, 40, 40),
64 => new Vector4(368, 342, 40, 40),
65 => new Vector4(408, 342, 40, 40),
66 => new Vector4(448, 342, 40, 40),
67 => new Vector4(360, 382, 40, 40),
68 => new Vector4(400, 382, 40, 40),
70 => new Vector4(360, 422, 40, 40),
71 => new Vector4(400, 422, 40, 40),
72 => new Vector4(440, 422, 40, 40),
73 => new Vector4(440, 382, 40, 40),
74 => new Vector4(216, 462, 40, 40),
75 => new Vector4(256, 462, 40, 40),
76 => new Vector4(296, 462, 40, 40),
77 => new Vector4(336, 462, 40, 40),
78 => new Vector4(376, 462, 40, 40),
79 => new Vector4(416, 462, 40, 40),
80 => new Vector4(456, 462, 40, 40),
81 => new Vector4(200, 502, 40, 40),
82 => new Vector4(240, 502, 40, 40),
83 => new Vector4(280, 502, 40, 40),
84 => new Vector4(320, 502, 40, 40),
85 => new Vector4(360, 502, 40, 40),
86 => new Vector4(400, 502, 40, 40),
87 => new Vector4(440, 502, 40, 40),
88 => new Vector4(200, 542, 40, 40),
89 => new Vector4(240, 542, 40, 40),
90 => new Vector4(280, 542, 40, 40),
91 => new Vector4(320, 542, 40, 40),
92 => new Vector4(360, 542, 40, 40),
93 => new Vector4(400, 542, 40, 40),
94 => new Vector4(440, 542, 40, 40),
95 => new Vector4(0, 582, 40, 40),
96 => new Vector4(40, 582, 40, 40),
97 => new Vector4(80, 582, 40, 40),
98 => new Vector4(120, 582, 40, 40),
99 => new Vector4(160, 582, 40, 40),
100 => new Vector4(200, 582, 40, 40),
101 => new Vector4(240, 582, 40, 40),
102 => new Vector4(280, 582, 40, 40),
103 => new Vector4(320, 582, 40, 40),
104 => new Vector4(360, 582, 40, 40),
105 => new Vector4(400, 582, 40, 40),
106 => new Vector4(440, 582, 40, 40),
107 => new Vector4(0, 622, 40, 40),
108 => new Vector4(40, 622, 40, 40),
109 => new Vector4(80, 622, 40, 40),
110 => new Vector4(120, 622, 40, 40),
111 => new Vector4(160, 622, 40, 40),
112 => new Vector4(200, 622, 40, 40),
_ => null,
};
// From Kizer: https://github.com/Soreepeong/Dalamud/blob/feature/log-wordwrap/Dalamud/Interface/Spannables/Internal/GfdFileView.cs
public readonly unsafe ref struct GfdFileView
{
private readonly ReadOnlySpan<byte> Span;
private readonly bool DirectLookup;
/// <summary>Initializes a new instance of the <see cref="GfdFileView"/> struct.</summary>
/// <param name="span">The data.</param>
public GfdFileView(ReadOnlySpan<byte> span)
{
Span = span;
if (span.Length < sizeof(GfdHeader))
throw new InvalidDataException($"Not enough space for a {nameof(GfdHeader)}");
if (span.Length < sizeof(GfdHeader) + (Header.Count * sizeof(GfdEntry)))
throw new InvalidDataException($"Not enough space for all the {nameof(GfdEntry)}");
var entries = Entries;
DirectLookup = true;
for (var i = 0; i < entries.Length && DirectLookup; i++)
DirectLookup &= i + 1 == entries[i].Id;
}
/// <summary>Gets the header.</summary>
private ref readonly GfdHeader Header => ref MemoryMarshal.AsRef<GfdHeader>(Span);
/// <summary>Gets the entries.</summary>
private ReadOnlySpan<GfdEntry> Entries => MemoryMarshal.Cast<byte, GfdEntry>(Span[sizeof(GfdHeader)..]);
/// <summary>Attempts to get an entry.</summary>
/// <param name="iconId">The icon ID.</param>
/// <param name="entry">The entry.</param>
/// <param name="followRedirect">Whether to follow redirects.</param>
/// <returns><c>true</c> if found.</returns>
public bool TryGetEntry(uint iconId, out GfdEntry entry, bool followRedirect = true)
{
if (iconId == 0)
{
entry = default;
return false;
}
var entries = Entries;
if (DirectLookup)
{
if (iconId <= entries.Length)
{
entry = entries[(int)(iconId - 1)];
return !entry.IsEmpty;
}
entry = default;
return false;
}
var lo = 0;
var hi = entries.Length;
while (lo <= hi)
{
var i = lo + ((hi - lo) >> 1);
if (entries[i].Id == iconId)
{
if (followRedirect && entries[i].Redirect != 0)
{
iconId = entries[i].Redirect;
lo = 0;
hi = entries.Length;
continue;
}
entry = entries[i];
return !entry.IsEmpty;
}
if (entries[i].Id < iconId)
lo = i + 1;
else
hi = i - 1;
}
entry = default;
return false;
}
/// <summary>Header of a .gfd file.</summary>
[StructLayout(LayoutKind.Sequential)]
public struct GfdHeader
{
/// <summary>Signature: "gftd0100".</summary>
public fixed byte Signature[8];
/// <summary>Number of entries.</summary>
public int Count;
/// <summary>Unused/unknown.</summary>
public fixed byte Padding[4];
}
/// <summary>An entry of a .gfd file.</summary>
[StructLayout(LayoutKind.Sequential, Size = 0x10)]
public struct GfdEntry
{
/// <summary>ID of the entry.</summary>
public ushort Id;
/// <summary>The left offset of the entry.</summary>
public ushort Left;
/// <summary>The top offset of the entry.</summary>
public ushort Top;
/// <summary>The width of the entry.</summary>
public ushort Width;
/// <summary>The height of the entry.</summary>
public ushort Height;
/// <summary>Unknown/unused.</summary>
public ushort Unk0A;
/// <summary>The redirected entry, maybe.</summary>
public ushort Redirect;
/// <summary>Unknown/unused.</summary>
public ushort Unk0E;
/// <summary>Gets a value indicating whether this entry is effectively empty.</summary>
public bool IsEmpty => Width == 0 || Height == 0;
}
}
internal static class IconUtil {
private static byte[]? GfdFile;
public static unsafe GfdFileView GfdFileView
{
get
{
GfdFile ??= Plugin.DataManager.GetFile("common/font/gfdata.gfd")!.Data;
return new GfdFileView(new ReadOnlySpan<byte>(Unsafe.AsPointer(ref GfdFile[0]), GfdFile.Length));
}
}
}