Merge branch 'main' into dean/copy-to-clipboard
This commit is contained in:
@@ -0,0 +1,6 @@
|
||||
[*]
|
||||
indent_style = space
|
||||
tab_width = 4
|
||||
indent_size = 4
|
||||
trim_trailing_whitespace = true
|
||||
insert_final_newline = true
|
||||
@@ -59,6 +59,19 @@ internal class TextChunk : Chunk {
|
||||
public TextChunk() : base(ChunkSource.None, null) {
|
||||
}
|
||||
#pragma warning restore CS8618
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new TextChunk with identical styling to this one.
|
||||
/// </summary>
|
||||
public TextChunk NewWithStyle(ChunkSource source, Payload? link, string content)
|
||||
{
|
||||
return new TextChunk(source, link, content) {
|
||||
FallbackColour = this.FallbackColour,
|
||||
Foreground = this.Foreground,
|
||||
Glow = this.Glow,
|
||||
Italic = this.Italic,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
internal class IconChunk : Chunk {
|
||||
|
||||
+88
-1
@@ -1,7 +1,9 @@
|
||||
using ChatTwo.Code;
|
||||
using ChatTwo.Util;
|
||||
using Dalamud.Game.Text.SeStringHandling;
|
||||
using Dalamud.Game.Text.SeStringHandling.Payloads;
|
||||
using LiteDB;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace ChatTwo;
|
||||
|
||||
@@ -70,7 +72,7 @@ internal class Message {
|
||||
this.Date = DateTime.UtcNow;
|
||||
this.Code = code;
|
||||
this.Sender = sender;
|
||||
this.Content = content;
|
||||
this.Content = ReplaceContentURLs(content);
|
||||
this.SenderSource = senderSource;
|
||||
this.ContentSource = contentSource;
|
||||
this.SortCode = new SortCode(this.Code.Type, this.Code.Source);
|
||||
@@ -89,6 +91,8 @@ internal class Message {
|
||||
this.Date = date;
|
||||
this.Code = BsonMapper.Global.ToObject<ChatCode>(code);
|
||||
this.Sender = BsonMapper.Global.Deserialize<List<Chunk>>(sender);
|
||||
// Don't call ReplaceContentURLs here since we're loading the message
|
||||
// from the database and it should already have parsed URL data.
|
||||
this.Content = BsonMapper.Global.Deserialize<List<Chunk>>(content);
|
||||
this.SenderSource = BsonMapper.Global.Deserialize<SeString>(senderSource);
|
||||
this.ContentSource = BsonMapper.Global.Deserialize<SeString>(contentSource);
|
||||
@@ -108,6 +112,8 @@ internal class Message {
|
||||
this.Date = date;
|
||||
this.Code = BsonMapper.Global.ToObject<ChatCode>(code);
|
||||
this.Sender = BsonMapper.Global.Deserialize<List<Chunk>>(sender);
|
||||
// Don't call ReplaceContentURLs here since we're loading the message
|
||||
// from the database and it should already have parsed URL data.
|
||||
this.Content = BsonMapper.Global.Deserialize<List<Chunk>>(content);
|
||||
this.SenderSource = BsonMapper.Global.Deserialize<SeString>(senderSource);
|
||||
this.ContentSource = BsonMapper.Global.Deserialize<SeString>(contentSource);
|
||||
@@ -138,4 +144,85 @@ internal class Message {
|
||||
|
||||
return Guid.Empty;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// URLRegex returns a regex object that matches URLs like:
|
||||
/// - https://example.com
|
||||
/// - http://example.com
|
||||
/// - www.example.com
|
||||
/// - https://sub.example.com
|
||||
/// - example.com
|
||||
/// - sub.example.com
|
||||
///
|
||||
/// It matches URLs with www. or https:// prefix, and also matches URLs
|
||||
/// without a prefix on specific TLDs.
|
||||
/// </summary>
|
||||
private static Regex URLRegex = new Regex(
|
||||
@"((https?:\/\/|www\.)[a-z0-9-]+(\.[a-z0-9-]+)*|([a-z0-9-]+(\.[a-z0-9-]+)*\.(com|net|org|co|io|app)))(:[\d]{1,5})?(\/[^\s]+)?",
|
||||
RegexOptions.Compiled | RegexOptions.IgnoreCase
|
||||
);
|
||||
|
||||
/// <summary>
|
||||
/// Finds all URL strings in all TextChunks, splits the parent TextChunk
|
||||
/// apart and inserts a new TextChunk with a URIPayload.
|
||||
/// </summary>
|
||||
private List<Chunk> ReplaceContentURLs(List<Chunk> content)
|
||||
{
|
||||
var newChunks = new List<Chunk>();
|
||||
void AddChunkWithMessage(Chunk chunk) {
|
||||
chunk.Message = this;
|
||||
newChunks.Add(chunk);
|
||||
}
|
||||
|
||||
foreach (var chunk in content)
|
||||
{
|
||||
// Use as is if it's not a text chunk or it already has a payload.
|
||||
if (chunk is not TextChunk text || chunk.Link != null)
|
||||
{
|
||||
// No need to call AddChunkWithMessage here since the chunk
|
||||
// already has the Message field set.
|
||||
newChunks.Add(chunk);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Find all URLs with the regex and insert a new TextChunk with a
|
||||
// URIPayload.
|
||||
var matches = URLRegex.Matches(text.Content);
|
||||
var remainderIndex = 0;
|
||||
foreach (Match match in matches.Cast<Match>())
|
||||
{
|
||||
// Add the text before the URL.
|
||||
if (match.Index > remainderIndex)
|
||||
{
|
||||
AddChunkWithMessage(text.NewWithStyle(chunk.Source, chunk.Link, text.Content[remainderIndex..match.Index]));
|
||||
}
|
||||
|
||||
// Update the remainder index.
|
||||
remainderIndex = match.Index + match.Length;
|
||||
|
||||
// Create a new TextChunk with a URIPayload for the URL text.
|
||||
try
|
||||
{
|
||||
var link = URIPayload.ResolveURI(match.Value);
|
||||
AddChunkWithMessage(text.NewWithStyle(chunk.Source, link, match.Value));
|
||||
}
|
||||
catch (UriFormatException)
|
||||
{
|
||||
Plugin.Log.Debug($"Invalid URL accepted by Regex but failed URI parsing: '{match.Value}'");
|
||||
// If the URL is invalid, set the remainder index to the
|
||||
// beginning of the match so it'll get included in the next
|
||||
// regular text chunk.
|
||||
remainderIndex = match.Index;
|
||||
}
|
||||
}
|
||||
|
||||
// Add the text after the last URL.
|
||||
if (remainderIndex < text.Content.Length)
|
||||
{
|
||||
AddChunkWithMessage(text.NewWithStyle(chunk.Source, null, text.Content[remainderIndex..]));
|
||||
}
|
||||
}
|
||||
|
||||
return newChunks;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,6 +21,7 @@ using Lumina.Excel.GeneratedSheets;
|
||||
using Action = System.Action;
|
||||
using DalamudPartyFinderPayload = Dalamud.Game.Text.SeStringHandling.Payloads.PartyFinderPayload;
|
||||
using ChatTwoPartyFinderPayload = ChatTwo.Util.PartyFinderPayload;
|
||||
using System.Diagnostics;
|
||||
|
||||
namespace ChatTwo;
|
||||
|
||||
@@ -86,6 +87,11 @@ public sealed class PayloadHandler {
|
||||
drawn = true;
|
||||
break;
|
||||
}
|
||||
case URIPayload uri: {
|
||||
DrawUriPopup(uri);
|
||||
drawn = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
ContextFooter(drawn, chunk);
|
||||
@@ -103,9 +109,7 @@ public sealed class PayloadHandler {
|
||||
ImGui.Separator();
|
||||
|
||||
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();
|
||||
@@ -229,6 +233,11 @@ public sealed class PayloadHandler {
|
||||
DoHover(() => HoverItem(item), hoverSize);
|
||||
break;
|
||||
}
|
||||
case URIPayload uri:
|
||||
{
|
||||
DoHover(() => HoverURI(uri), hoverSize);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -348,6 +357,11 @@ public sealed class PayloadHandler {
|
||||
}
|
||||
}
|
||||
|
||||
private void HoverURI(URIPayload uri) {
|
||||
ImGui.TextUnformatted(string.Format(Language.Context_URLDomain, uri.Uri.Authority));
|
||||
ImGuiUtil.WarningText(Language.Context_URLWarning);
|
||||
}
|
||||
|
||||
private void LeftClickPayload(Chunk chunk, Payload? payload) {
|
||||
switch (payload) {
|
||||
case MapLinkPayload map: {
|
||||
@@ -386,6 +400,10 @@ public sealed class PayloadHandler {
|
||||
|
||||
break;
|
||||
}
|
||||
case URIPayload uri: {
|
||||
TryOpenURI(uri.Uri);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -639,4 +657,38 @@ public sealed class PayloadHandler {
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private void DrawUriPopup(URIPayload uri)
|
||||
{
|
||||
ImGui.TextUnformatted(string.Format(Language.Context_URLDomain, uri.Uri.Authority));
|
||||
ImGuiUtil.WarningText(Language.Context_URLWarning, false);
|
||||
ImGui.Separator();
|
||||
|
||||
if (ImGui.Selectable(Language.Context_OpenInBrowser))
|
||||
{
|
||||
TryOpenURI(uri.Uri);
|
||||
}
|
||||
|
||||
if (ImGui.Selectable(Language.Context_CopyLink))
|
||||
{
|
||||
ImGui.SetClipboardText(uri.Uri.ToString());
|
||||
WrapperUtil.AddNotification(Language.Context_CopyLinkNotification, NotificationType.Info);
|
||||
}
|
||||
}
|
||||
|
||||
private void TryOpenURI(Uri uri)
|
||||
{
|
||||
new Thread(() => {
|
||||
try
|
||||
{
|
||||
Plugin.Log.Info($"Opening URI {uri} in default browser");
|
||||
Process.Start(new ProcessStartInfo(uri.ToString()) { UseShellExecute = true });
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Plugin.Log.Error($"Error opening URI: {ex}");
|
||||
WrapperUtil.AddNotification(Language.Context_OpenInBrowserError, NotificationType.Error);
|
||||
}
|
||||
}).Start();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
Generated
+53
@@ -1067,6 +1067,23 @@ namespace ChatTwo.Resources {
|
||||
}
|
||||
}
|
||||
|
||||
/// Looks up a localized string similar to Copy link to clipboard.
|
||||
/// </summary>
|
||||
internal static string Context_CopyLink {
|
||||
get {
|
||||
return ResourceManager.GetString("Context_CopyLink", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Copied link to clipboard.
|
||||
/// </summary>
|
||||
internal static string Context_CopyLinkNotification {
|
||||
get {
|
||||
return ResourceManager.GetString("Context_CopyLinkNotification", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Hide chat.
|
||||
/// </summary>
|
||||
@@ -1148,6 +1165,24 @@ namespace ChatTwo.Resources {
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Open link in browser.
|
||||
/// </summary>
|
||||
internal static string Context_OpenInBrowser {
|
||||
get {
|
||||
return ResourceManager.GetString("Context_OpenInBrowser", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Failed to open the link in the browser, please report this issue.
|
||||
/// </summary>
|
||||
internal static string Context_OpenInBrowserError {
|
||||
get {
|
||||
return ResourceManager.GetString("Context_OpenInBrowserError", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Promote.
|
||||
/// </summary>
|
||||
@@ -1229,6 +1264,24 @@ namespace ChatTwo.Resources {
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to URL at {0}.
|
||||
/// </summary>
|
||||
internal static string Context_URLDomain {
|
||||
get {
|
||||
return ResourceManager.GetString("Context_URLDomain", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Only open URLs from websites you trust.
|
||||
/// </summary>
|
||||
internal static string Context_URLWarning {
|
||||
get {
|
||||
return ResourceManager.GetString("Context_URLWarning", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Chinese (full).
|
||||
/// </summary>
|
||||
|
||||
@@ -871,6 +871,7 @@
|
||||
<data name="Options_TooltipOffset_Desc">
|
||||
<value>Use this option if you experience cut-off tooltips.</value>
|
||||
</data>
|
||||
|
||||
<data name="Context_CopyContent">
|
||||
<value>Copy content</value>
|
||||
</data>
|
||||
@@ -880,4 +881,22 @@
|
||||
<data name="Context_CopySuccess">
|
||||
<value>Copied message to clipboard</value>
|
||||
</data>
|
||||
<data name="Context_CopyLink" xml:space="preserve">
|
||||
<value>Copy link to clipboard</value>
|
||||
</data>
|
||||
<data name="Context_CopyLinkNotification" xml:space="preserve">
|
||||
<value>Copied link to clipboard</value>
|
||||
</data>
|
||||
<data name="Context_OpenInBrowser" xml:space="preserve">
|
||||
<value>Open link in browser</value>
|
||||
</data>
|
||||
<data name="Context_OpenInBrowserError" xml:space="preserve">
|
||||
<value>Failed to open the link in the browser, please report this issue</value>
|
||||
</data>
|
||||
<data name="Context_URLDomain" xml:space="preserve">
|
||||
<value>URL at {0}</value>
|
||||
</data>
|
||||
<data name="Context_URLWarning" xml:space="preserve">
|
||||
<value>Only open URLs from websites you trust</value>
|
||||
</data>
|
||||
</root>
|
||||
|
||||
@@ -86,6 +86,11 @@ internal class Store : IDisposable {
|
||||
["Type"] = new("PartyFinder"),
|
||||
["Id"] = new(partyFinder.Id),
|
||||
});
|
||||
case URIPayload uri:
|
||||
return new BsonDocument(new Dictionary<string, BsonValue> {
|
||||
["Type"] = new("URI"),
|
||||
["Uri"] = new(uri.Uri.ToString()),
|
||||
});
|
||||
}
|
||||
|
||||
return payload?.Encode();
|
||||
@@ -99,6 +104,7 @@ internal class Store : IDisposable {
|
||||
return bson["Type"].AsString switch {
|
||||
"Achievement" => new AchievementPayload((uint) bson["Id"].AsInt64),
|
||||
"PartyFinder" => new PartyFinderPayload((uint) bson["Id"].AsInt64),
|
||||
"URI" => new URIPayload(new Uri(bson["Uri"].AsString)),
|
||||
_ => null,
|
||||
};
|
||||
}
|
||||
@@ -257,6 +263,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 +272,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) {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
using ChatTwo.Code;
|
||||
using Dalamud.Game.Text.SeStringHandling;
|
||||
using Dalamud.Game.Text.SeStringHandling.Payloads;
|
||||
using System.Text;
|
||||
|
||||
namespace ChatTwo.Util;
|
||||
|
||||
@@ -68,15 +69,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();
|
||||
}
|
||||
else if (rawPayload.Data.Length > 6 && rawPayload.Data[2] == 0x05 && rawPayload.Data[3] == 0xF6)
|
||||
if (colorPayload.Enabled)
|
||||
{
|
||||
var (r, g, b) = (rawPayload.Data[4], rawPayload.Data[5], rawPayload.Data[6]);
|
||||
foreground.Push(ColourUtil.ComponentsToRgba(r, g, b));
|
||||
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)
|
||||
@@ -100,6 +105,10 @@ internal static class ChunkUtil {
|
||||
var reader = new BinaryReader(new MemoryStream(rawPayload.Data[4..]));
|
||||
var id = GetInteger(reader);
|
||||
link = new AchievementPayload(id);
|
||||
} else if (rawPayload.Data.Length > 5 && rawPayload.Data[1] == 0x27 && rawPayload.Data[3] == 0x07) {
|
||||
// uri payload
|
||||
var uri = new Uri(Encoding.UTF8.GetString(rawPayload.Data[4..]));
|
||||
link = new URIPayload(uri);
|
||||
} else if (Equals(rawPayload, RawPayload.LinkTerminator)) {
|
||||
link = null;
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
@@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -197,16 +197,16 @@ internal static class ImGuiUtil {
|
||||
}
|
||||
}
|
||||
|
||||
internal static void WarningText(string text) {
|
||||
internal static void WarningText(string text, bool wrap = true) {
|
||||
var style = StyleModel.GetConfiguredStyle() ?? StyleModel.GetFromCurrent();
|
||||
var dalamudOrange = style.BuiltInColors?.DalamudOrange;
|
||||
if (dalamudOrange != null) {
|
||||
ImGui.PushStyleColor(ImGuiCol.Text, dalamudOrange.Value);
|
||||
}
|
||||
|
||||
ImGui.PushTextWrapPos();
|
||||
if (wrap) ImGui.PushTextWrapPos();
|
||||
ImGui.TextUnformatted(text);
|
||||
ImGui.PopTextWrapPos();
|
||||
if (wrap) ImGui.PopTextWrapPos();
|
||||
|
||||
if (dalamudOrange != null) {
|
||||
ImGui.PopStyleColor();
|
||||
|
||||
@@ -37,3 +37,51 @@ internal class AchievementPayload : Payload {
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
internal class URIPayload(Uri uri) : Payload
|
||||
{
|
||||
public override PayloadType Type => (PayloadType) 0x52;
|
||||
|
||||
public Uri Uri { get; init; } = uri;
|
||||
|
||||
private static readonly string[] ExpectedSchemes = ["http", "https"];
|
||||
private static readonly string DefaultScheme = "https";
|
||||
|
||||
/// <summary>
|
||||
/// Create a URIPayload from a raw URI string. If the URI does not have a
|
||||
/// scheme, it will default to https://.
|
||||
/// </summary>
|
||||
/// <exception cref="UriFormatException">
|
||||
/// If the URI is invalid, or if the scheme is not supported.
|
||||
/// </exception>
|
||||
public static URIPayload ResolveURI(string rawURI)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(rawURI);
|
||||
|
||||
// Check for expected scheme ://, if not add https://
|
||||
foreach (var scheme in ExpectedSchemes)
|
||||
{
|
||||
if (rawURI.StartsWith($"{scheme}://"))
|
||||
{
|
||||
return new URIPayload(new Uri(rawURI));
|
||||
}
|
||||
}
|
||||
if (rawURI.Contains("://"))
|
||||
{
|
||||
throw new UriFormatException($"Unsupported scheme in URL: {rawURI}");
|
||||
}
|
||||
|
||||
return new URIPayload(new Uri($"{DefaultScheme}://{rawURI}"));
|
||||
}
|
||||
|
||||
protected override void DecodeImpl(BinaryReader reader, long endOfStream)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
protected override byte[] EncodeImpl()
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user