Skip to content
Open
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
64 changes: 64 additions & 0 deletions ChatTwo/Ipc/StyleIpc.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
using Dalamud.Plugin.Ipc;

namespace ChatTwo.Ipc;

using MessageStyle = (uint BackgroundRgba, float Alpha);

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

private ICallGateProvider<int> VersionGate { get; }
private ICallGateProvider<string, object?> SetProviderGate { 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;

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);
}

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
5 changes: 5 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
52 changes: 52 additions & 0 deletions ChatTwo/Ui/ChatLog/ChatLog.Window.cs
Original file line number Diff line number Diff line change
Expand Up @@ -652,6 +652,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 Down Expand Up @@ -681,6 +685,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)
{
message.IsVisible[tab.Identifier] = false;
continue;
}

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

var applyBackground = message.StyleBackground != 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.
using var styleAlpha = ImRaii.PushStyle(ImGuiStyleVar.Alpha, ImGui.GetStyle().Alpha * message.StyleAlpha, message.StyleAlpha < 1f);

if (tab.DisplayTimestamp)
{
var localTime = message.Date.ToLocalTime();
Expand Down Expand Up @@ -809,6 +847,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 +867,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
100 changes: 100 additions & 0 deletions ipc.md
Original file line number Diff line number Diff line change
Expand Up @@ -127,3 +127,103 @@ public sealed class TypingIntegration {
```

All integrations are called inside of an ImGui `BeginMenu`.

# Message Styling IPC

Plugins can style individual messages in the Chat 2 log — a per-message
background colour, fading, or hiding a message from display (e.g. group
highlighting or fading chat from far-away players). Hidden and faded messages
are only affected visually: they are still stored in the database, exported,
and shown in the web interface.

- `ChatTwo.StyleVersion`: call this function to retrieve the styling IPC
version (currently `1`).
- `ChatTwo.SetMessageStyleProvider`: call this action with the name of an IPC
gate **your** plugin provides. Chat 2 will invoke that gate once per
incoming message. Pass an empty string to unregister. Only one provider is
active at a time (last writer wins).
- Re-register when the `ChatTwo.Available` event fires (Chat 2 was loaded or
updated).

Your provider gate must have this signature:

```
(string senderName, string senderWorld, ulong contentId, ushort chatType, string senderRaw, string contentText)
→ (uint BackgroundRgba, float Alpha)
```

- `senderName` / `senderWorld`: name and home world taken from the first
`PlayerPayload` in the sender SeString; empty strings if the sender is not a
player.
- `contentId`: the sender's content ID, or 0 if unknown. Use it transiently
only — the plugin submission rules forbid storing other players' content
IDs.
- `chatType`: the raw `XivChatType` / LogKind value of the message.
- `senderRaw`: the full sender text including party position or friend-group
glyphs and cross-world affixes.
- `contentText`: the message content as plain text (payloads stripped).
- Returns: `BackgroundRgba` as `RR GG BB AA` (use `0` for no background) and
`Alpha` (`1` = normal, between `0` and `1` = faded, `<= 0` = hidden from the
log).

Notes:

- The provider is called **once per message at ingestion, on a background
thread** — it must be thread-safe, must not assume the framework thread, and
should return quickly. If it throws, the message renders unstyled.
- Styles are fixed at ingestion. Provider configuration changes only affect
new messages, and messages loaded from the database render unstyled.
- With the timestamp table layout ("prettier timestamps") the background tints
the whole row; in the classic layout it is drawn behind the message text.

Example:

```cs
public sealed class StyleIntegration : IDisposable {
private ICallGateSubscriber<int> StyleVersion { get; }
private ICallGateSubscriber<string, object?> SetMessageStyleProvider { get; }
private ICallGateSubscriber<object?> Available { get; }
private ICallGateProvider<string, string, ulong, ushort, string, string, (uint BackgroundRgba, float Alpha)> Provider { get; }

public StyleIntegration(IDalamudPluginInterface pluginInterface) {
StyleVersion = pluginInterface.GetIpcSubscriber<int>("ChatTwo.StyleVersion");
SetMessageStyleProvider = pluginInterface.GetIpcSubscriber<string, object?>("ChatTwo.SetMessageStyleProvider");
Available = pluginInterface.GetIpcSubscriber<object?>("ChatTwo.Available");

Provider = pluginInterface.GetIpcProvider<string, string, ulong, ushort, string, string, (uint BackgroundRgba, float Alpha)>("MyPlugin.MessageStyle");
Provider.RegisterFunc(GetStyle);

// Re-register when Chat 2 loads or updates, and register now in case
// it is already loaded.
Available.Subscribe(Register);
Register();
}

private void Register() {
try {
if (StyleVersion.InvokeFunc() == 1)
SetMessageStyleProvider.InvokeAction("MyPlugin.MessageStyle");
} catch {
// Chat 2 is not loaded.
}
}

private (uint BackgroundRgba, float Alpha) GetStyle(string senderName, string senderWorld, ulong contentId, ushort chatType, string senderRaw, string contentText) {
// Tint messages from a specific player, fade everyone on Novice
// Network, leave everything else untouched.
if (senderName == "Haurchefant Greystone")
return (0x0080FF30u, 1f);

return (XivChatType) chatType == XivChatType.NoviceNetwork ? (0u, 0.5f) : (0u, 1f);
}

public void Dispose() {
Available.Unsubscribe(Register);
try {
SetMessageStyleProvider.InvokeAction("");
} catch {
// Chat 2 is not loaded.
}
}
}
```