feat(chat): port retail client command families

Expand the typed client-command boundary across travel, character queries, local UI and layout controls, AFK and consent, emotes, friends, squelch and filters, and fill-components. Preserve retail packet layouts and queue ownership, import the confirmation dialog, and keep authoritative social state in Core.

Co-Authored-By: Codex <codex@openai.com>
This commit is contained in:
Erik 2026-07-13 14:50:15 +02:00
parent 5a45a7ac7f
commit 9ea579bdd0
47 changed files with 6659 additions and 99 deletions

View file

@ -311,6 +311,7 @@ public sealed class UiText : UiElement
if (y < top || y + lh > bottom) continue; // whole-line vertical clip (no scissor yet)
string text = lines[i].Text;
float lineX = HorizontalOffset(text, datFont, bitmapFont);
// Selection highlight behind this line's selected character span.
if (hasSel && i >= selStart.Line && i <= selEnd.Line)
@ -324,12 +325,12 @@ public sealed class UiText : UiElement
float hx, hw;
if (datFont is not null)
{
hx = Padding + datFont.MeasureWidth(text.Substring(0, c0));
hx = lineX + datFont.MeasureWidth(text.Substring(0, c0));
hw = datFont.MeasureWidth(text.Substring(c0, c1 - c0));
}
else
{
hx = Padding + bitmapFont!.MeasureWidth(text.Substring(0, c0));
hx = lineX + bitmapFont!.MeasureWidth(text.Substring(0, c0));
hw = bitmapFont.MeasureWidth(text.Substring(c0, c1 - c0));
}
// Highlight sits BEHIND the line's text → sprite bucket, submitted
@ -339,12 +340,24 @@ public sealed class UiText : UiElement
}
if (datFont is not null)
ctx.DrawStringDat(datFont, text, Padding, y, lines[i].Color);
ctx.DrawStringDat(datFont, text, lineX, y, lines[i].Color);
else
ctx.DrawString(text, Padding, y, lines[i].Color, bitmapFont);
ctx.DrawString(text, lineX, y, lines[i].Color, bitmapFont);
}
}
private float HorizontalOffset(string text, UiDatFont? datFont, BitmapFont? bitmapFont)
{
float width = datFont is not null
? datFont.MeasureWidth(text)
: bitmapFont?.MeasureWidth(text) ?? 0f;
if (Centered)
return Math.Max(Padding, (Width - width) * 0.5f);
if (RightAligned)
return Math.Max(Padding, Width - Padding - width);
return Padding;
}
public override bool OnEvent(in UiEvent e)
{
switch (e.Type)
@ -545,16 +558,80 @@ public sealed class UiText : UiElement
line = Math.Clamp(line, 0, lines.Count - 1);
string text = lines[line].Text;
float lineX = HorizontalOffset(text, _lastDatFont, _lastFont);
int col = _lastDatFont is { } df
? CharIndexAt(text, ch => df.TryGetGlyph(ch, out var g) ? UiDatFont.GlyphAdvance(g) : 0f,
localX - _lastPadding)
localX - lineX)
: (_lastFont is { } bf
? CharIndexAt(text, ch => bf.TryGetGlyph(ch, out var bg) ? bg.Advance : 0f,
localX - _lastPadding)
localX - lineX)
: 0);
return new Pos(line, col);
}
/// <summary>Word-wrap text to a measured pixel width, preserving explicit newlines.</summary>
public static IReadOnlyList<string> WrapWords(
string text,
Func<string, float> measureWidth,
float maximumWidth)
{
ArgumentNullException.ThrowIfNull(text);
ArgumentNullException.ThrowIfNull(measureWidth);
if (maximumWidth <= 0f) throw new ArgumentOutOfRangeException(nameof(maximumWidth));
var result = new List<string>();
string[] paragraphs = text.Replace("\r", string.Empty).Split('\n');
foreach (string paragraph in paragraphs)
{
if (paragraph.Length == 0)
{
result.Add(string.Empty);
continue;
}
string[] words = paragraph.Split(' ', StringSplitOptions.RemoveEmptyEntries);
var line = new StringBuilder();
foreach (string word in words)
{
string candidate = line.Length == 0 ? word : $"{line} {word}";
if (measureWidth(candidate) <= maximumWidth)
{
if (line.Length != 0) line.Append(' ');
line.Append(word);
continue;
}
if (line.Length != 0 && measureWidth(word) <= maximumWidth)
{
result.Add(line.ToString());
line.Clear();
line.Append(word);
continue;
}
// Retail GlyphList wrapping can split an over-width glyph run.
// Pack as much of the long token as possible onto the current
// line, then continue at character boundaries without hyphens.
for (int i = 0; i < word.Length; i++)
{
string prefix = i == 0 && line.Length != 0 ? " " : string.Empty;
if (line.Length != 0
&& measureWidth(line + prefix + word[i]) > maximumWidth)
{
result.Add(line.ToString());
line.Clear();
prefix = string.Empty;
}
line.Append(prefix).Append(word[i]);
}
}
result.Add(line.ToString());
}
return result;
}
/// <summary>
/// The caret column for a horizontal position <paramref name="x"/> (already
/// adjusted for the left padding, so x=0 is the start of the text). Walks the