The exit-world confirmation (ID_Client_EndCharacterSessionConfirm, table
0x23000001 key 0x0EB1C41D) rendered its literal two-character "\n" escapes
because escape decoding lived in individual consumers — Batch E centralized
it for authored captions only (DatWidgetFactory.ResolveAuthoredString), and
each new string surface had to remember its own copy. The installed DAT
carries the escape in 4,365 of 7,050 strings; per-consumer normalization
was structurally guaranteed to keep leaking.
Retail's placement is the SOURCE, not the widget: every public StringInfo
resolution ends in StringTableMetaLanguage::UnescapeString @ 0x0067BDC0
(StringInfo::InqString @ 0x0042E490, GetLiteralValue @ 0x0042CA50), the
write side escapes (SetLiteralValue @ 0x0042C980; AddVariable_String
@ 0x0042E6C0 for template variables), and widgets receive decoded text.
Ported exactly:
- NEW RetailStringEscapes: UnescapeString/EscapeString + the
GetUnEscapedChar @ 0x0067B750 / GetEscapedChar @ 0x0067B6C0 tables
(\n \t \r \q + the ten metalanguage self-escapes []!{}#\|^$,
byte-verified against the PDB-paired 2013 binary at 0x3FE178;
unrecognized pairs stay verbatim).
- DatStringResolver.Resolve/ResolveAll unescape at the source;
ResolveTemplate escapes each variable on insert and unescapes the
composed whole — retail's round trip, so variable content (player
names) can never be corrupted by the final decode.
- RETIRED the consumer copies (double paths would corrupt an authored
"\n" into a line break): DatWidgetFactory.NormalizeEscapes + BuildText's
inline replace, RetailUiRuntime.NormalizeRetailNewlines + the
OpenCaptureInstructions inline replace, DatRichText.Compose's replace,
IndicatorDetailText.Shape's replace. ItemAppraisalTextLayout's replace
stays — WIRE-domain (server strings never pass the DAT source; retail's
ItemExamineUI::AddItemInfo @ 0x004AC050 appends wire text verbatim), now
documented as such.
- Consumer CR-strips retired with them: the installed DATs contain ZERO
real CR characters (sweep-measured) and UiText.WrapWords already drops
strays.
Tests: RetailStringEscapes conformance (escape set, unknown pairs,
round trip), DatStringResolver source-decode pins (including the exact
user-reported exit-world text shape and a backslash-carrying variable),
the installed-DAT escape sweep (7,050 strings; every resolution must equal
the retail unescape of the raw entry; inventory printed), and the existing
caption/rich-text/live-DAT pins relocated to the source contract.
App 5550/3 (live-DAT), Runtime 1747/0, complete Release solution green
across all suites.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
45 lines
1.8 KiB
C#
45 lines
1.8 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
|
|
namespace AcDream.App.UI.Layout;
|
|
|
|
/// <summary>
|
|
/// Shared text shaping for retail indicator detail pages. Retail
|
|
/// <c>UIElement_Text::SetText</c> reflows newline-delimited strings into the
|
|
/// authored field width; acdream's retained text seam carries already-shaped
|
|
/// lines, so controllers perform the equivalent shaping before publication.
|
|
/// </summary>
|
|
internal static class IndicatorDetailText
|
|
{
|
|
public static IReadOnlyList<UiText.Line> Shape(UiText target, string text)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(target);
|
|
text ??= string.Empty;
|
|
|
|
float maxWidth = Math.Max(1f, target.Width - (2f * target.Padding));
|
|
float Measure(string value)
|
|
=> target.DatFont?.MeasureWidth(value)
|
|
?? target.Font?.MeasureWidth(value)
|
|
?? value.Length * 8f;
|
|
|
|
var lines = new List<UiText.Line>();
|
|
// DAT-resolved bodies (vitae, link status, effects) arrive with
|
|
// real line breaks — escapes decode at the string source
|
|
// (DatStringResolver → RetailStringEscapes, 2026-08-17 systemic
|
|
// round). Wire-sourced text (the appraisal inscription) renders
|
|
// verbatim, exactly like retail's ItemExamineUI::AddItemInfo
|
|
// @ 0x004AC050 → UIElement_Text::AppendTextWithFont direct append.
|
|
foreach (string paragraph in text.Split('\n'))
|
|
{
|
|
if (paragraph.Length == 0)
|
|
{
|
|
lines.Add(new UiText.Line(string.Empty, target.DefaultColor));
|
|
continue;
|
|
}
|
|
|
|
foreach (string line in ChatTranscriptRenderer.WrapText(paragraph, maxWidth, Measure))
|
|
lines.Add(new UiText.Line(line, target.DefaultColor));
|
|
}
|
|
return lines;
|
|
}
|
|
}
|