feat(messages): render sender names through the name-aware path
This commit is contained in:
@@ -385,6 +385,7 @@ public sealed class Plugin : IAsyncDalamudPlugin
|
||||
new SelfTests.PerformanceBaselineStep(this),
|
||||
new SelfTests.MainWindowFocusOpacityStep(this),
|
||||
new SelfTests.MainWindowFlagsStep(this),
|
||||
new SelfTests.SenderNameReformatStep(this),
|
||||
]);
|
||||
|
||||
// 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() { }
|
||||
}
|
||||
@@ -38,6 +38,13 @@ internal sealed class ChunkRenderer
|
||||
_ = _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(
|
||||
IReadOnlyList<Chunk> chunks,
|
||||
bool wrap = true,
|
||||
@@ -51,7 +58,25 @@ internal sealed class ChunkRenderer
|
||||
// the list unchanged when nothing applies, so non-sender lists and the
|
||||
// neutral default cost only a quick scan.
|
||||
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);
|
||||
|
||||
|
||||
@@ -88,12 +88,25 @@ internal sealed class MessageList
|
||||
|
||||
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 sender = message.SenderSource.TextValue;
|
||||
ImGui.TextUnformatted(
|
||||
string.IsNullOrEmpty(sender) ? timestamp : $"{timestamp} {sender}: "
|
||||
);
|
||||
ImGui.SameLine(0f, 0f);
|
||||
if (message.Sender.Count > 0)
|
||||
{
|
||||
ImGui.TextUnformatted($"{timestamp} ");
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -128,9 +141,22 @@ internal sealed class MessageList
|
||||
|
||||
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 sender = message.SenderSource.TextValue;
|
||||
ImGui.TextUnformatted(string.IsNullOrEmpty(sender) ? timestamp : $"{timestamp} {sender}");
|
||||
if (message.Sender.Count > 0)
|
||||
{
|
||||
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);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user