diff --git a/HellionChat/Plugin.cs b/HellionChat/Plugin.cs index d942de0..b980c88 100755 --- a/HellionChat/Plugin.cs +++ b/HellionChat/Plugin.cs @@ -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 diff --git a/HellionChat/SelfTests/SenderNameReformatStep.cs b/HellionChat/SelfTests/SenderNameReformatStep.cs new file mode 100644 index 0000000..2e7c063 --- /dev/null +++ b/HellionChat/SelfTests/SenderNameReformatStep.cs @@ -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 + { + 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() { } +} diff --git a/HellionChat/Ui/Components/ChunkRenderer.cs b/HellionChat/Ui/Components/ChunkRenderer.cs index e6ae5e3..5439523 100644 --- a/HellionChat/Ui/Components/ChunkRenderer.cs +++ b/HellionChat/Ui/Components/ChunkRenderer.cs @@ -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 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); diff --git a/HellionChat/Ui/Components/MessageList.cs b/HellionChat/Ui/Components/MessageList.cs index 786354b..da9d3b7 100644 --- a/HellionChat/Ui/Components/MessageList.cs +++ b/HellionChat/Ui/Components/MessageList.cs @@ -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); }