fix(ui): systemic escape normalization at the string source
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>
This commit is contained in:
parent
fdc4fd496d
commit
967b9c57cf
15 changed files with 605 additions and 116 deletions
|
|
@ -2973,10 +2973,10 @@ public sealed class RetailUiRuntime : IDisposable
|
|||
});
|
||||
if (text is null) return 0u; // no invented English
|
||||
// The authored text stores its blank line as a literal
|
||||
// "\n\n" two-character escape (live-probed) — same
|
||||
// convention DatWidgetFactory/IndicatorDetailText already
|
||||
// unescape for other DAT-authored strings.
|
||||
text = text.Replace("\\n", "\n", StringComparison.Ordinal);
|
||||
// "\n\n" two-character escape (live-probed), decoded at
|
||||
// the string source (DatStringResolver →
|
||||
// RetailStringEscapes, 2026-08-17 systemic round) —
|
||||
// `text` arrives with real line breaks.
|
||||
try
|
||||
{
|
||||
return DialogFactory.MakeWait(text, queueKey: 0x10000001u);
|
||||
|
|
@ -4331,13 +4331,13 @@ public sealed class RetailUiRuntime : IDisposable
|
|||
{
|
||||
lock (_bindings.Assets.DatLock)
|
||||
{
|
||||
return NormalizeRetailNewlines(strings.ResolveTemplate(
|
||||
stringTableId,
|
||||
"ID_CharacterManagement_DeleteCharacterConfirmation",
|
||||
new Dictionary<uint, string>
|
||||
{
|
||||
[DatStringResolver.PlayerVariable] = characterName,
|
||||
})!);
|
||||
return strings.ResolveTemplate(
|
||||
stringTableId,
|
||||
"ID_CharacterManagement_DeleteCharacterConfirmation",
|
||||
new Dictionary<uint, string>
|
||||
{
|
||||
[DatStringResolver.PlayerVariable] = characterName,
|
||||
})!;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -4353,16 +4353,16 @@ public sealed class RetailUiRuntime : IDisposable
|
|||
confirmExit));
|
||||
}
|
||||
|
||||
// Escape decoding (the DAT's literal two-character "\n" and friends)
|
||||
// happens at the string source since the 2026-08-17 systemic round —
|
||||
// DatStringResolver → RetailStringEscapes, retail's own placement — so
|
||||
// this is a plain key-hash resolve. The former NormalizeRetailNewlines
|
||||
// consumer copy is retired (double-decoding corrupts an authored "\\n").
|
||||
private static string? ResolveCharacterManagementString(
|
||||
DatStringResolver strings,
|
||||
uint tableId,
|
||||
string key) =>
|
||||
strings.Resolve(tableId, DatStringResolver.ComputeHash(key)) is { } value
|
||||
? NormalizeRetailNewlines(value)
|
||||
: null;
|
||||
|
||||
private static string NormalizeRetailNewlines(string value) =>
|
||||
value.Replace("\\n", "\n", StringComparison.Ordinal);
|
||||
strings.Resolve(tableId, DatStringResolver.ComputeHash(key));
|
||||
|
||||
private void ConfigureCharacterCreation()
|
||||
{
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue