Files
HellionChat/HellionChat/Util/SenderNameDisplay.cs
T
JonKazama-Hellion 1d3b429f1b
Security / scan (push) Successful in 23s
Build / Build (Release) (push) Successful in 31s
Forge Announce / Post changelog to Hellion Forge (push) Successful in 6s
Release / Build and attach release ZIP (push) Successful in 41s
style(format): apply csharpier and markdownlint reflow
2026-05-23 09:07:01 +02:00

104 lines
4.1 KiB
C#

using System.Collections.Generic;
using Dalamud.Game.Text.SeStringHandling.Payloads;
using HellionChat._Helpers;
namespace HellionChat.Util;
// UI-7: produces a render-only view of a chunk list with the sender's name
// reformatted per the user's WorldSuffixMode / NameFormMode. Called from
// ChatLogWindow.DrawChunks on every draw — it never mutates the input, so the
// stored message (the Sender BLOB in the DB) stays byte-for-byte unchanged and
// a later settings change reformats history too.
// Known trade-off: at non-default settings, this allocates one List<Chunk> per
// visible message per frame. Lists are small and the path is skipped at the
// neutral defaults, so GC pressure is low in practice. Accepted; noted in
// Cycle Notes.
internal static class SenderNameDisplay
{
// Returns a copy of the list with the whole ChunkSource.Sender span
// collapsed to one formatted chunk, or the original list when nothing
// should change (neutral defaults, no sender span, or a non-player sender).
internal static IReadOnlyList<Chunk> ForDisplay(IReadOnlyList<Chunk> chunks)
{
var suffixMode = Plugin.Config.WorldSuffixMode;
var formMode = Plugin.Config.NameFormMode;
// Neutral default = today's rendering. Bail before scanning so the
// common case stays free.
if (suffixMode == WorldSuffixMode.OtherWorldOnly && formMode == NameFormMode.Full)
return chunks;
// Locate the sender span. A content list has no Sender chunks and is
// returned untouched.
var first = -1;
var last = -1;
for (var i = 0; i < chunks.Count; i++)
{
if (chunks[i].Source != ChunkSource.Sender)
continue;
if (first < 0)
first = i;
last = i;
}
if (first < 0)
return chunks;
// The PlayerPayload rides on the sender chunks; a system or non-player
// sender has none and is left alone.
PlayerPayload? payload = null;
for (var i = first; i <= last; i++)
{
if (chunks[i].Link is PlayerPayload pp)
{
payload = pp;
break;
}
}
if (payload is null)
return chunks;
var worldName = payload.World.ValueNullable?.Name.ExtractText() ?? string.Empty;
// IPlayerState.HomeWorld reads AgentLobby directly without a
// framework-thread guard (reference_dalamud_framework_thread), so this
// call is safe from the draw thread.
// RowId is a value field — safe to read directly without ValueNullable, unlike the world name above.
var isHomeWorld = payload.World.RowId == Plugin.PlayerState.HomeWorld.RowId;
var formatted = SenderNameFormatter.Format(
payload.PlayerName,
worldName,
isHomeWorld,
formMode,
suffixMode
);
// Render-only copy: replace the whole sender span (name text, world
// text, and any cross-world icon) with one formatted chunk that keeps
// the PlayerPayload link so the name stays clickable. Dropping the
// original sender icon is an accepted trade-off and only happens once
// the user moves UI-7 off its defaults.
// Channel brackets and colons are ChunkSource.None wrappers outside the
// Sender span — they are preserved untouched by the copy loops below.
var copy = new List<Chunk>(chunks.Count);
for (var i = 0; i < first; i++)
copy.Add(chunks[i]);
TextChunk? styleSource = null;
for (var i = first; i <= last; i++)
{
if (chunks[i] is TextChunk tc)
{
styleSource = tc;
break;
}
}
var replacement = styleSource is not null
? styleSource.NewWithStyle(ChunkSource.Sender, payload, formatted)
: new TextChunk(ChunkSource.Sender, payload, formatted);
copy.Add(replacement);
for (var i = last + 1; i < chunks.Count; i++)
copy.Add(chunks[i]);
return copy;
}
}