From b6ae08d91d56b1e60ff6873fa467337689880f7f Mon Sep 17 00:00:00 2001 From: Dean Sheather Date: Sun, 12 May 2024 07:19:03 +1000 Subject: [PATCH] chore: move message processing to thread again --- ChatTwo/MessageManager.cs | 98 ++++++++++++++-------- ChatTwo/Util/ExtraPayload.cs | 114 +++++++++----------------- ChatTwo/Util/GlobalParametersCache.cs | 46 +++++++++++ 3 files changed, 148 insertions(+), 110 deletions(-) create mode 100644 ChatTwo/Util/GlobalParametersCache.cs diff --git a/ChatTwo/MessageManager.cs b/ChatTwo/MessageManager.cs index 350b4d7..6e0eb0d 100644 --- a/ChatTwo/MessageManager.cs +++ b/ChatTwo/MessageManager.cs @@ -21,7 +21,7 @@ internal class MessageManager : IAsyncDisposable private Dictionary Formats { get; } = new(); private ulong LastContentId { get; set; } - private ConcurrentQueue Pending { get; } = new(); + private ConcurrentQueue Pending { get; } = new(); private ulong LastMessageIndex { get; set; } private readonly Thread PendingMessageThread; @@ -152,10 +152,52 @@ internal class MessageManager : IAsyncDisposable { LastMessage = (sender, message); - var chatCode = new ChatCode((ushort)type); + var pendingMessage = new PendingMessage + { + ReceiverId = CurrentContentId, + ContentId = 0, + Type = type, + SenderId = senderId, + Sender = sender, + Content = message, + }; + + // Update colour codes. + GlobalParametersCache.Refresh(); + + // If the message was rendered in the vanilla chat log window it has an + // index, and we can use that to get the sender's content ID. The + // content ID is used to show "invite to party" buttons in the context + // menu. + var idx = Plugin.Functions.GetCurrentChatLogEntryIndex() ?? 0; + var shouldGetContentId = false; + if (idx > LastMessageIndex) + { + LastMessageIndex = idx; + shouldGetContentId = true; + } + + // You can't call GetContentIdForEntry in the same framework tick + // that you received the message, or you just get null. + // + // We delay all messages to be enqueued in the next framework tick + // because of this. We used to only delay messages that we wanted to + // fetch a content ID for, but this results in out-of-order messages + // occasionally. + Plugin.Framework.RunOnTick(() => + { + if (shouldGetContentId) + pendingMessage.ContentId = Plugin.Functions.Chat.GetContentIdForEntry(idx - 1); + Pending.Enqueue(pendingMessage); + }); + } + + private void ProcessMessage(PendingMessage pendingMessage) + { + var chatCode = new ChatCode((ushort)pendingMessage.Type); NameFormatting? formatting = null; - if (sender.Payloads.Count > 0) + if (pendingMessage.Sender.Payloads.Count > 0) formatting = FormatFor(chatCode.Type); var senderChunks = new List(); @@ -165,50 +207,26 @@ internal class MessageManager : IAsyncDisposable { FallbackColour = chatCode.Type, }); - senderChunks.AddRange(ChunkUtil.ToChunks(sender, ChunkSource.Sender, chatCode.Type)); + senderChunks.AddRange(ChunkUtil.ToChunks(pendingMessage.Sender, ChunkSource.Sender, chatCode.Type)); senderChunks.Add(new TextChunk(ChunkSource.None, null, formatting.After) { FallbackColour = chatCode.Type, }); } - var contentChunks = ChunkUtil.ToChunks(message, ChunkSource.Content, chatCode.Type).ToList(); - var msg = new Message(CurrentContentId, 0, chatCode, senderChunks, contentChunks, sender, message); + var contentChunks = ChunkUtil.ToChunks(pendingMessage.Content, ChunkSource.Content, chatCode.Type).ToList(); + var message = new Message(CurrentContentId, pendingMessage.ContentId, chatCode, senderChunks, contentChunks, pendingMessage.Sender, pendingMessage.Content); - // If the message was rendered in the vanilla chat log window it has an - // index, and we can use that to get the sender's content ID. The - // content ID is used to show "invite to party" buttons in the context - // menu. - var idx = Plugin.Functions.GetCurrentChatLogEntryIndex() ?? 0; - if (idx > LastMessageIndex) - { - LastMessageIndex = idx; - // You can't call GetContentIdForEntry in the same framework tick - // that you received the message, or you just get null. - Plugin.Framework.RunOnTick(() => - { - msg.ContentId = Plugin.Functions.Chat.GetContentIdForEntry(idx - 1); - Pending.Enqueue(msg); - }); + if (Plugin.Config.DatabaseBattleMessages || !message.Code.IsBattle()) + Store.UpsertMessage(message); - return; - } - - Pending.Enqueue(msg); - } - - private void ProcessMessage(Message msg) - { - if (Plugin.Config.DatabaseBattleMessages || !msg.Code.IsBattle()) - Store.UpsertMessage(msg); - - var currentMatches = Plugin.ChatLogWindow.CurrentTab?.Matches(msg) ?? false; + var currentMatches = Plugin.ChatLogWindow.CurrentTab?.Matches(message) ?? false; foreach (var tab in Plugin.Config.Tabs) { var unread = !(tab.UnreadMode == UnreadMode.Unseen && Plugin.ChatLogWindow.CurrentTab != tab && currentMatches); - if (tab.Matches(msg)) - tab.AddMessage(msg, unread); + if (tab.Matches(message)) + tab.AddMessage(message, unread); } } @@ -272,4 +290,14 @@ internal class MessageManager : IAsyncDisposable return nameFormatting; } + + private class PendingMessage + { + internal ulong ReceiverId { get; set; } + internal ulong ContentId { get; set; } // 0 if unknown + internal XivChatType Type { get; set; } + internal uint SenderId { get; set; } + internal SeString Sender { get; set; } + internal SeString Content { get; set; } + } } diff --git a/ChatTwo/Util/ExtraPayload.cs b/ChatTwo/Util/ExtraPayload.cs index 9e736c9..27842c1 100644 --- a/ChatTwo/Util/ExtraPayload.cs +++ b/ChatTwo/Util/ExtraPayload.cs @@ -22,88 +22,52 @@ public class ColorPayload var typeByte = stream.ReadByte(); var payload = new ColorPayload(); - if (typeByte == 0xEC) + switch (typeByte) { - payload.Enabled = false; - return payload; - } - - if (typeByte == 0xE9) - { - var param = stream.ReadByte(); - var ok = TryGetGNumDefault((uint) (param - 2), out var value); - if (!ok) + case 0xEC: + payload.Enabled = false; + return payload; + case 0xE9: { - Plugin.Log.Error($"Unable to GetGNum for param {param - 2}"); - return null; + var param = stream.ReadByte(); + var value = (uint) GlobalParametersCache.GetValue(param - 2); + payload.Enabled = true; + payload.UnshiftedColor = value; + payload.Color = ColourUtil.ArgbToRgba(value); + + return payload; } - - 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) + case >= 0xF0 and <= 0xFE: { - return v switch + // From: https://github.com/NotAdam/Lumina/blob/master/src/Lumina/Text/Expressions/IntegerExpression.cs#L119-L128 + uint ShiftAndThrowIfZero(int v, int shift) { - -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 - }; + 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; } - - 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; + return null; } } } \ No newline at end of file diff --git a/ChatTwo/Util/GlobalParametersCache.cs b/ChatTwo/Util/GlobalParametersCache.cs new file mode 100644 index 0000000..9c5208f --- /dev/null +++ b/ChatTwo/Util/GlobalParametersCache.cs @@ -0,0 +1,46 @@ +using Dalamud.Utility; +using FFXIVClientStructs.FFXIV.Client.UI.Misc; +using FFXIVClientStructs.FFXIV.Component.Text; + +namespace ChatTwo.Util; + +public static class GlobalParametersCache +{ + private static int[] Cache = []; + + public static int GetValue(int index) + { + if (index < 0 || index >= Cache.Length) + return 0; + return Cache[index]; + } + + /// + /// Refresh the cache of global parameters from RaptureTextModule. + /// + /// + /// This should be called in the main thread when updates are necessary. + /// + public static unsafe void Refresh() + { + if (!ThreadSafety.IsMainThread) + throw new InvalidOperationException("GlobalParametersCache.Refresh must be called on the main thread."); + + var rtm = RaptureTextModule.Instance(); + if (rtm is null) + return; + + ref var gp = ref rtm->TextModule.MacroDecoder.GlobalParameters; + + if (Cache.Length != (int)gp.MySize) + Cache = new int[gp.MySize]; + for (ulong i = 0; i < gp.MySize; i++) + { + var p = gp.Get(i); + if (p.Type == TextParameterType.Integer) + Cache[(int)i] = p.IntValue; + else + Cache[(int)i] = 0; + } + } +} \ No newline at end of file