Files
HellionChat/HellionChat/Util/IconUtil.cs
T
2026-05-11 08:11:30 +02:00

156 lines
4.5 KiB
C#
Executable File

using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using SixLabors.ImageSharp;
using SixLabors.ImageSharp.PixelFormats;
namespace HellionChat.Util;
// 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;
// span: raw .gfd file bytes
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;
}
private ref readonly GfdHeader Header => ref MemoryMarshal.AsRef<GfdHeader>(Span);
private ReadOnlySpan<GfdEntry> Entries =>
MemoryMarshal.Cast<byte, GfdEntry>(Span[sizeof(GfdHeader)..]);
// Returns true if the entry was found.
// followRedirect: whether to chase redirect chains.
public bool TryGetEntry(uint iconId, out GfdEntry entry, bool followRedirect = true)
{
if (iconId == 0)
{
entry = default;
return false;
}
var entries = Entries;
if (DirectLookup)
{
// Follow redirects on the direct-lookup path for consistency with
// the binary-search path.
var visited = 0;
while (iconId <= entries.Length)
{
entry = entries[(int)(iconId - 1)];
if (followRedirect && entry.Redirect != 0 && entry.Redirect != iconId)
{
if (++visited > entries.Length)
break; // cycle guard
iconId = entry.Redirect;
continue;
}
return !entry.IsEmpty;
}
entry = default;
return false;
}
if (entries.Length == 0)
{
entry = default;
return false;
}
var lo = 0;
var hi = entries.Length - 1;
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 - 1;
continue;
}
entry = entries[i];
return !entry.IsEmpty;
}
if (entries[i].Id < iconId)
lo = i + 1;
else
hi = i - 1;
}
entry = default;
return false;
}
// .gfd file header
[StructLayout(LayoutKind.Sequential)]
public struct GfdHeader
{
public fixed byte Signature[8]; // "gftd0100"
public int Count;
public fixed byte Padding[4];
}
// .gfd file entry -- one icon slot
[StructLayout(LayoutKind.Sequential, Size = 0x10)]
public struct GfdEntry
{
public ushort Id;
public ushort Left;
public ushort Top;
public ushort Width;
public ushort Height;
public ushort Unk0A;
public ushort Redirect; // non-zero = redirects to another entry
public ushort Unk0E;
public bool IsEmpty => Width == 0 || Height == 0;
}
}
internal static class IconUtil
{
private static byte[]? GfdFile;
public static GfdFileView GfdFileView
{
get
{
if (GfdFile is null)
{
var file =
Plugin.DataManager.GetFile("common/font/gfdata.gfd")
?? throw new FileNotFoundException(
"Failed to load common/font/gfdata.gfd from the game data."
);
GfdFile = file.Data;
}
return new GfdFileView(GfdFile);
}
}
public static byte[] ImageToRaw(this Image<Rgba32> image)
{
var data = new byte[4 * image.Width * image.Height];
image.CopyPixelDataTo(data);
return data;
}
}