104 lines
4.1 KiB
C#
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;
|
|
}
|
|
}
|