Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion ChatTwo/Configuration.cs
Original file line number Diff line number Diff line change
Expand Up @@ -286,7 +286,10 @@ public class Tab

[NonSerialized] public UsedChannel CurrentChannel = new();

[NonSerialized] public Guid Identifier = Guid.NewGuid();
// Persisted so per-tab settings from other plugins (tab style policies)
// survive restarts. Configs saved before this field existed get a fresh
// Guid on load, which sticks with the next save.
public Guid Identifier = Guid.NewGuid();

public bool Matches(Message message)
{
Expand Down
115 changes: 115 additions & 0 deletions ChatTwo/Ipc/StyleIpc.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
using Dalamud.Plugin.Ipc;

namespace ChatTwo.Ipc;

using MessageStyle = (uint BackgroundRgba, float Alpha);

public sealed class StyleIpc : IDisposable
{
private const int Version = 1;

// Suppress-flags for SetTabStylePolicies; a tab without an entry has all
// styling enabled.
public const int PolicySuppressBackground = 1;
public const int PolicySuppressFade = 2;
public const int PolicySuppressHide = 4;

private ICallGateProvider<int> VersionGate { get; }
private ICallGateProvider<string, object?> SetProviderGate { get; }
private ICallGateProvider<Dictionary<Guid, string>> GetTabsGate { get; }
private ICallGateProvider<Dictionary<Guid, string>, object?> TabsChangedGate { get; }
private ICallGateProvider<Dictionary<Guid, int>, object?> SetTabPoliciesGate { get; }

// Written from the registering plugin's thread, read from the message
// processing thread; reference reads/writes are atomic.
private ICallGateSubscriber<string, string, ulong, ushort, string, string, MessageStyle>? Provider;

// Swapped as a whole on writes, so render-thread reads are safe.
private Dictionary<Guid, int> TabPolicies = new();

public bool HasProvider => Provider != null;

public StyleIpc()
{
VersionGate = Plugin.Interface.GetIpcProvider<int>("ChatTwo.StyleVersion");
VersionGate.RegisterFunc(() => Version);

SetProviderGate = Plugin.Interface.GetIpcProvider<string, object?>("ChatTwo.SetMessageStyleProvider");
SetProviderGate.RegisterAction(SetProvider);

GetTabsGate = Plugin.Interface.GetIpcProvider<Dictionary<Guid, string>>("ChatTwo.GetTabs");
GetTabsGate.RegisterFunc(BuildTabList);

TabsChangedGate = Plugin.Interface.GetIpcProvider<Dictionary<Guid, string>, object?>("ChatTwo.TabsChanged");

SetTabPoliciesGate = Plugin.Interface.GetIpcProvider<Dictionary<Guid, int>, object?>("ChatTwo.SetTabStylePolicies");
// Copy defensively: the render thread reads this dictionary, so it
// must not share state with (or be a null from) the calling plugin.
SetTabPoliciesGate.RegisterAction(policies => TabPolicies = policies is null ? [] : new Dictionary<Guid, int>(policies));
}

private static Dictionary<Guid, string> BuildTabList()
=> Plugin.Config.Tabs
.Where(tab => !tab.IsTempTab)
.ToDictionary(tab => tab.Identifier, tab => tab.Name);

/// <summary>
/// Pushes the current tab list to subscribers; called whenever the
/// configuration is saved.
/// </summary>
public void NotifyTabsChanged()
{
try
{
TabsChangedGate.SendMessage(BuildTabList());
}
catch (Exception ex)
{
// SendMessage runs subscribers synchronously and does not isolate
// their exceptions; a broken consumer must not break config saves.
Plugin.Log.Warning(ex, "Error in a ChatTwo.TabsChanged subscriber");
}
}

/// <summary>
/// Suppress-flags for a tab; 0 means all styling is enabled.
/// </summary>
public int GetTabPolicy(Guid tabIdentifier)
=> TabPolicies.GetValueOrDefault(tabIdentifier, 0);

private void SetProvider(string gateName)
{
Provider = string.IsNullOrEmpty(gateName)
? null
: Plugin.Interface.GetIpcSubscriber<string, string, ulong, ushort, string, string, MessageStyle>(gateName);
}

/// <summary>
/// Asks the registered style provider for a background colour (RGBA, 0 for
/// none) and alpha (1 = normal, 0 &lt; a &lt; 1 = faded, &lt;= 0 = hidden
/// from the log) for a message. Called once per message on the message
/// processing thread; providers must be thread-safe.
/// </summary>
public MessageStyle Evaluate(string senderName, string senderWorld, ulong contentId, ushort chatType, string senderRaw, string contentText)
{
if (Provider is not { } provider)
return (0, 1f);

try
{
return provider.InvokeFunc(senderName, senderWorld, contentId, chatType, senderRaw, contentText);
}
catch (Exception)
{
// A broken or unloaded provider must never break message
// ingestion; unstyled is the safe fallback.
return (0, 1f);
}
}

public void Dispose()
{
SetProviderGate.UnregisterAction();
VersionGate.UnregisterFunc();
}
}
5 changes: 5 additions & 0 deletions ChatTwo/Message.cs
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,11 @@ public partial class Message
public Dictionary<Guid, float?> Height { get; } = new();
public Dictionary<Guid, bool> IsVisible { get; } = new();

// Set once at ingestion via the message styling IPC; messages loaded from
// the database render unstyled.
public uint StyleBackground; // RGBA, 0 = no background
public float StyleAlpha = 1f; // <= 0 hides the message from the log (it stays stored)

public Message(ulong receiver, ulong contentId, ulong accountId, ChatCode code, List<Chunk> sender, List<Chunk> content, SeString senderSource, SeString contentSource)
{
var extraChatChannel = ExtractExtraChatChannel(contentSource);
Expand Down
13 changes: 13 additions & 0 deletions ChatTwo/MessageManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
using Dalamud.Game.Chat;
using Dalamud.Game.Text;
using Dalamud.Game.Text.SeStringHandling;
using Dalamud.Game.Text.SeStringHandling.Payloads;
using Dalamud.Hooking;
using Dalamud.Interface.ImGuiNotification;
using Dalamud.Plugin.Services;
Expand Down Expand Up @@ -276,6 +277,18 @@ private void ProcessMessage(PendingMessage pendingMessage)
var contentChunks = ChunkUtil.ToChunks(pendingMessage.Content, ChunkSource.Content, chatCode.Type).ToList();
var message = new Message(CurrentContentId, pendingMessage.ContentId, pendingMessage.AccountId, chatCode, senderChunks, contentChunks, pendingMessage.Sender, pendingMessage.Content);

if (Plugin.StyleIpc.HasProvider)
{
var senderPayload = pendingMessage.Sender.Payloads.OfType<PlayerPayload>().FirstOrDefault();
(message.StyleBackground, message.StyleAlpha) = Plugin.StyleIpc.Evaluate(
senderPayload?.PlayerName ?? "",
senderPayload?.World.ValueNullable?.Name.ExtractText() ?? "",
pendingMessage.ContentId,
(ushort) pendingMessage.LogKind,
pendingMessage.Sender.TextValue,
pendingMessage.Content.TextValue);
}

var isBattle = message.Code.IsBattle();
var isCraftOrGather = message.Code.IsCraftOrGather();
if ((!isBattle && !isCraftOrGather) || (isBattle && Plugin.Config.DatabaseBattleMessages) || (isCraftOrGather && Plugin.Config.DatabaseGatherCraftMessages))
Expand Down
9 changes: 9 additions & 0 deletions ChatTwo/Plugin.cs
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ public sealed class Plugin : IDalamudPlugin
public IpcManager Ipc { get; }
public ExtraChat ExtraChat { get; }
public TypingIpc TypingIpc { get; }
public StyleIpc StyleIpc { get; }
public FontManager FontManager { get; }

public readonly ServerCore ServerCore;
Expand Down Expand Up @@ -130,6 +131,9 @@ public Plugin()

Commands = new Commands();
Functions = new GameFunctions.GameFunctions(this);
// Constructed before IpcManager so the style gates are already
// registered when the ChatTwo.Available broadcast fires.
StyleIpc = new StyleIpc();
Ipc = new IpcManager();
TypingIpc = new TypingIpc(this);
ExtraChat = new ExtraChat();
Expand Down Expand Up @@ -217,6 +221,7 @@ public void Dispose()
SeStringDebugger?.Dispose();

TypingIpc?.Dispose();
StyleIpc?.Dispose();
ExtraChat?.Dispose();
Ipc?.Dispose();
MessageManager?.DisposeAsync().AsTask().Wait();
Expand Down Expand Up @@ -255,6 +260,10 @@ private void Draw()
public void SaveConfig()
{
Interface.SavePluginConfig(Config);
// Config saves are the choke point for tab changes (add, rename,
// delete, reorder). StyleIpc is null while migrations save during
// load.
StyleIpc?.NotifyTabsChanged();
}

public void LanguageChanged(string langCode)
Expand Down
59 changes: 59 additions & 0 deletions ChatTwo/Ui/ChatLog/ChatLog.Window.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
using ChatTwo.Code;
using ChatTwo.GameFunctions;
using ChatTwo.GameFunctions.Types;
using ChatTwo.Ipc;
using ChatTwo.Resources;
using ChatTwo.Ui.Handler;
using ChatTwo.Util;
Expand Down Expand Up @@ -652,6 +653,10 @@ private void DrawLogTableStyle(Tab tab, PayloadHandler handler, bool switchedTab

private void DrawMessages(Tab tab, PayloadHandler handler, bool isTable, bool moreCompact = false, float oldCellPaddingY = 0)
{
// Set while the classic-layout background has the draw list split, so
// an exception mid-message can't leave the list split (the next
// frame's split would then assert inside ImGui).
var backgroundSplitActive = false;
try
{
// This may produce ApplicationException which is catched below.
Expand All @@ -670,6 +675,8 @@ private void DrawMessages(Tab tab, PayloadHandler handler, bool isTable, bool mo
int? lastMessageHash = null;
var sameCount = 0;

var tabPolicy = Plugin.StyleIpc.GetTabPolicy(tab.Identifier);

var maxLines = Plugin.Config.MaxLinesToRender;
var startLine = messages.Count > maxLines ? messages.Count - maxLines : 0;
for (var i = startLine; i < messages.Count; i++)
Expand All @@ -681,6 +688,16 @@ private void DrawMessages(Tab tab, PayloadHandler handler, bool isTable, bool mo
message.IsVisible[tab.Identifier] = false;
}

// Hidden by the message style provider; the message stays
// stored, logged and exported. Skipped before duplicate
// collapsing so a hidden message neither anchors nor counts
// toward a collapse run.
if (message.StyleAlpha <= 0 && (tabPolicy & StyleIpc.PolicySuppressHide) == 0)
{
message.IsVisible[tab.Identifier] = false;
continue;
}

if (Plugin.Config.CollapseDuplicateMessages)
{
var messageHash = message.Hash;
Expand Down Expand Up @@ -760,6 +777,34 @@ [new TextChunk(ChunkSource.None, null, $" ({sameCount + 1}x)") { FallbackColor =
message.IsVisible[tab.Identifier] = nowVisible;
}

var applyBackground = message.StyleBackground != 0 && (tabPolicy & StyleIpc.PolicySuppressBackground) == 0;
if (applyBackground && isTable)
ImGui.TableSetBgColor(ImGuiTableBgTarget.RowBg0, ColourUtil.RgbaToAbgr(message.StyleBackground));

// In the classic layout the background's size is only known
// after the message is drawn (word wrapping), so draw the text
// into the upper draw list channel and fill the rect behind it
// afterwards.
var drawList = ImGui.GetWindowDrawList();
var splitBackground = applyBackground && !isTable;
var backgroundStart = Vector2.Zero;
var backgroundWidth = 0f;
if (splitBackground)
{
backgroundStart = ImGui.GetCursorScreenPos();
backgroundWidth = ImGui.GetContentRegionAvail().X;
drawList.ChannelsSplit(2);
drawList.ChannelsSetCurrent(1);
backgroundSplitActive = true;
}

// Faded by the message style provider. A message that reaches
// this point with alpha <= 0 had its hiding suppressed by the
// tab policy and renders fully visible.
var effectiveStyleAlpha = message.StyleAlpha <= 0 ? 1f : message.StyleAlpha;
var fade = effectiveStyleAlpha < 1f && (tabPolicy & StyleIpc.PolicySuppressFade) == 0;
using var styleAlpha = ImRaii.PushStyle(ImGuiStyleVar.Alpha, ImGui.GetStyle().Alpha * effectiveStyleAlpha, fade);

if (tab.DisplayTimestamp)
{
var localTime = message.Date.ToLocalTime();
Expand Down Expand Up @@ -809,6 +854,15 @@ [new TextChunk(ChunkSource.None, null, $" ({sameCount + 1}x)") { FallbackColor =
InputHandler.ChunkHandler.DrawChunks(message.Content, true, handler, lineWidth);

message.IsVisible[tab.Identifier] = ImGui.IsItemVisible();

if (splitBackground)
{
drawList.ChannelsSetCurrent(0);
var backgroundEnd = new Vector2(backgroundStart.X + backgroundWidth, ImGui.GetItemRectMax().Y);
drawList.AddRectFilled(backgroundStart, backgroundEnd, ColourUtil.RgbaToAbgr(message.StyleBackground));
drawList.ChannelsMerge();
backgroundSplitActive = false;
}
}
}
catch (ApplicationException)
Expand All @@ -820,6 +874,11 @@ [new TextChunk(ChunkSource.None, null, $" ({sameCount + 1}x)") { FallbackColor =
{
Plugin.Log.Warning(ex, "Error drawing chat log");
}
finally
{
if (backgroundSplitActive)
ImGui.GetWindowDrawList().ChannelsMerge();
}
}

private void DrawTabBar()
Expand Down
Loading