feat(messages): render sender names through the name-aware path

This commit is contained in:
2026-05-31 00:31:48 +02:00
parent 8fcb10cf51
commit 83b1708d5d
4 changed files with 141 additions and 8 deletions
+1
View File
@@ -385,6 +385,7 @@ public sealed class Plugin : IAsyncDalamudPlugin
new SelfTests.PerformanceBaselineStep(this), new SelfTests.PerformanceBaselineStep(this),
new SelfTests.MainWindowFocusOpacityStep(this), new SelfTests.MainWindowFocusOpacityStep(this),
new SelfTests.MainWindowFlagsStep(this), new SelfTests.MainWindowFlagsStep(this),
new SelfTests.SenderNameReformatStep(this),
]); ]);
// Re-surface the wizard for existing users when a major UX // Re-surface the wizard for existing users when a major UX
@@ -0,0 +1,81 @@
using System.Collections.Generic;
using Dalamud.Bindings.ImGui;
using Dalamud.Game.Text.SeStringHandling.Payloads;
using Dalamud.Plugin.SelfTest;
namespace HellionChat.SelfTests;
// B2-1/B2-2: proves the WorldSuffixMode/NameFormMode reformat reaches the REAL
// render entry. Drives ChunkRenderer.DrawChunks (a SelfTests/README-sanctioned
// real entry that wires SenderNameDisplay.ForDisplay at ChunkRenderer.cs:54)
// with a synthetic ChunkSource.Sender chunk carrying a PlayerPayload, at a
// non-neutral NameFormMode, and reads the LastRenderedSenderText observability
// the real draw produced. NameFormMode.Initials + WorldSuffixMode.Never is
// world-independent ("Test Tester" -> "T. T."), so the assertion is
// deterministic without a live world lookup. The MessageList routing (its row
// methods pass message.Sender to DrawChunks) is gated by the reviewer-grep
// (Step 2.6) + in-game smoke, since the visible sender change needs real chat +
// the world sheet. Does NOT call SenderNameFormatter/ForDisplay in isolation
// (the false-green trap — both are green today on a path the message list never
// takes for the sender).
internal sealed class SenderNameReformatStep : ISelfTestStep
{
private readonly Plugin plugin;
public SenderNameReformatStep(Plugin plugin)
{
this.plugin = plugin;
}
public string Name => "Hellion Chat - sender name reformat";
public SelfTestStepResult RunStep()
{
var renderer = this.plugin.ChunkRenderer;
if (renderer is null)
{
ImGui.Text("Plugin.ChunkRenderer is null");
return SelfTestStepResult.Fail;
}
var savedForm = Plugin.Config.NameFormMode;
var savedSuffix = Plugin.Config.WorldSuffixMode;
var savedScreenshot = Plugin.Config.ScreenshotMode;
try
{
// Initials (non-neutral) so ForDisplay reformats; Never + screenshot
// off so the result is world-independent and the reformat is not
// skipped.
Plugin.Config.NameFormMode = NameFormMode.Initials;
Plugin.Config.WorldSuffixMode = WorldSuffixMode.Never;
Plugin.Config.ScreenshotMode = false;
// ForDisplay formats payload.PlayerName, not the chunk text.
var payload = new PlayerPayload("Test Tester", 1u);
var senderChunks = new List<Chunk>
{
new TextChunk(ChunkSource.Sender, payload, "Test Tester"),
};
renderer.DrawChunks(senderChunks);
if (renderer.LastRenderedSenderText != "T. T.")
{
ImGui.Text(
$"LastRenderedSenderText = '{renderer.LastRenderedSenderText}', expected 'T. T.' (Initials reformat through the real render path)"
);
return SelfTestStepResult.Fail;
}
return SelfTestStepResult.Pass;
}
finally
{
Plugin.Config.NameFormMode = savedForm;
Plugin.Config.WorldSuffixMode = savedSuffix;
Plugin.Config.ScreenshotMode = savedScreenshot;
}
}
public void CleanUp() { }
}
+26 -1
View File
@@ -38,6 +38,13 @@ internal sealed class ChunkRenderer
_ = _logger; _ = _logger;
} }
// B2-1/B2-2 render-observability: the formatted sender text the real draw
// path actually produced (post-ForDisplay). A SelfTest reads this after
// driving DrawChunks to prove the WorldSuffixMode/NameFormMode reformat
// reached the real render entry — never the helper in isolation. null until
// a sender span is reformatted for display.
internal string? LastRenderedSenderText { get; private set; }
public void DrawChunks( public void DrawChunks(
IReadOnlyList<Chunk> chunks, IReadOnlyList<Chunk> chunks,
bool wrap = true, bool wrap = true,
@@ -51,7 +58,25 @@ internal sealed class ChunkRenderer
// the list unchanged when nothing applies, so non-sender lists and the // the list unchanged when nothing applies, so non-sender lists and the
// neutral default cost only a quick scan. // neutral default cost only a quick scan.
if (!Plugin.Config.ScreenshotMode) if (!Plugin.Config.ScreenshotMode)
chunks = SenderNameDisplay.ForDisplay(chunks); {
var displayed = SenderNameDisplay.ForDisplay(chunks);
// ForDisplay only allocates a NEW list when it actually reformatted
// a sender span (same reference on the neutral default / non-sender
// lists). So this scan runs only when a sender name was reformatted
// for display — zero overhead on the neutral-default hot path.
if (!ReferenceEquals(displayed, chunks))
{
chunks = displayed;
foreach (var c in chunks)
{
if (c.Source == ChunkSource.Sender && c is TextChunk reformatted)
{
LastRenderedSenderText = reformatted.Content;
break;
}
}
}
}
using var style = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, Vector2.Zero); using var style = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, Vector2.Zero);
+33 -7
View File
@@ -88,12 +88,25 @@ internal sealed class MessageList
private void DrawCompactRow(Message message) private void DrawCompactRow(Message message)
{ {
// B2-1/B2-2: render the sender through DrawChunks (the name-aware path
// that applies WorldSuffixMode/NameFormMode via ForDisplay), not as a
// flat SenderSource.TextValue string. message.Sender already carries the
// channel brackets/colon as ChunkSource.None wrappers (MessageManager
// .cs:300-314), so the separator is rendered by the chunks. 1.5.6 parity
// (ChatLogWindow.cs:1965: DrawChunks(message.Sender) + SameLine).
var timestamp = FormatTimestamp(message.Date); var timestamp = FormatTimestamp(message.Date);
var sender = message.SenderSource.TextValue; if (message.Sender.Count > 0)
ImGui.TextUnformatted( {
string.IsNullOrEmpty(sender) ? timestamp : $"{timestamp} {sender}: " ImGui.TextUnformatted($"{timestamp} ");
); ImGui.SameLine(0f, 0f);
ImGui.SameLine(0f, 0f); _chunkRenderer.DrawChunks(message.Sender, wrap: true, handler: _handler, lineWidth: 0f);
ImGui.SameLine(0f, 0f);
}
else
{
ImGui.TextUnformatted(timestamp);
ImGui.SameLine(0f, 0f);
}
_chunkRenderer.DrawChunks(message.Content, wrap: true, handler: _handler, lineWidth: 0f); _chunkRenderer.DrawChunks(message.Content, wrap: true, handler: _handler, lineWidth: 0f);
} }
@@ -128,9 +141,22 @@ internal sealed class MessageList
private void DrawCardRow(Message message) private void DrawCardRow(Message message)
{ {
// B2-1/B2-2: sender via DrawChunks (name-aware path), on its own line
// with content below — 1.5.6 card parity (ChatLogWindow.cs:1913, no
// SameLine after the sender). The 1.5.6 channel-colour push on the
// sender is deferred styling polish (masterplan §6 -> v1.9.0); plain
// text here.
var timestamp = FormatTimestamp(message.Date); var timestamp = FormatTimestamp(message.Date);
var sender = message.SenderSource.TextValue; if (message.Sender.Count > 0)
ImGui.TextUnformatted(string.IsNullOrEmpty(sender) ? timestamp : $"{timestamp} {sender}"); {
ImGui.TextUnformatted($"{timestamp} ");
ImGui.SameLine(0f, 0f);
_chunkRenderer.DrawChunks(message.Sender, wrap: true, handler: _handler, lineWidth: 0f);
}
else
{
ImGui.TextUnformatted(timestamp);
}
_chunkRenderer.DrawChunks(message.Content, wrap: true, handler: _handler, lineWidth: 0f); _chunkRenderer.DrawChunks(message.Content, wrap: true, handler: _handler, lineWidth: 0f);
} }