using DatReaderWriter;
using AcDream.Content;
using DatReaderWriter.DBObjs;
namespace AcDream.App.UI.Layout;
///
/// Resolves retail StringInfo values through local.dat string tables.
/// The caller owns synchronization around reads.
///
///
///
/// Retail reference: StringInfo::GetString and
/// compute_str_hash @ 0x00413110. A StringInfo's token selects one
/// localized string variant; ordinary UI labels use token zero.
///
///
/// Every resolution decodes the DAT's two-character escapes
/// (\n, \t, \r, \q, and the metalanguage
/// self-escapes) HERE, at the source — retail's own placement: every public
/// StringInfo resolution ends in
/// StringTableMetaLanguage::UnescapeString @ 0x0067BDC0
/// (StringInfo::InqString @ 0x0042E490,
/// StringInfo::GetLiteralValue @ 0x0042CA50). Consumers receive
/// already-decoded text and must not re-decode — see
/// ' remarks for the double-decode hazard
/// (the 2026-08-17 systemic round that retired the per-consumer copies).
///
///
public sealed class DatStringResolver
{
private readonly IDatReaderWriter _dats;
private readonly Dictionary _tables = new();
public DatStringResolver(IDatReaderWriter dats)
=> _dats = dats ?? throw new ArgumentNullException(nameof(dats));
public string? Resolve(UiStringInfoValue info)
=> Resolve(info.TableId, info.StringId, info.Token);
public string? Resolve(uint tableId, uint stringId, int token = 0)
{
if (tableId == 0u || stringId == 0u)
return null;
if (!_tables.TryGetValue(tableId, out StringTable? table))
{
table = _dats.Get(tableId);
_tables[tableId] = table;
}
if (table is null
|| !table.Strings.TryGetValue(stringId, out var entry)
|| entry.Strings.Count == 0)
return null;
int index = token >= 0 && token < entry.Strings.Count ? token : 0;
// StringInfo::InqString @ 0x0042E490's unconditional tail: the stored
// string is escaped; the resolved string is decoded.
return RetailStringEscapes.Unescape(entry.Strings[index].Value);
}
/// Returns every literal token for one retail StringInfo entry.
public string[]? ResolveAll(uint tableId, uint stringId)
{
if (tableId == 0u || stringId == 0u)
return null;
if (!_tables.TryGetValue(tableId, out StringTable? table))
{
table = _dats.Get(tableId);
_tables[tableId] = table;
}
return table is not null
&& table.Strings.TryGetValue(stringId, out var entry)
&& entry.Strings.Count != 0
? entry.Strings
.Select(value => RetailStringEscapes.Unescape(value.Value))
.ToArray()
: null;
}
/// Retail's variable-name hash for PLAYER (0x05506DA2) —
/// the single substitution slot every social confirmation template uses.
public static readonly uint PlayerVariable = ComputeHash("PLAYER");
///
/// Composes a templated StringTable entry: N (or N+1) literal fragments
/// interleaved with N named variables, keyed by the entry-key hash.
///
///
/// Exact port of StringTable::GetString @ 0x004300D0's
/// no-metalanguage branch @ 0x004303B7: each fragment is appended,
/// then the variable in the same slot (resolved through
/// , keyed by of the
/// authored variable name; a missing variable substitutes the empty
/// string, as retail does). This is NOT a
/// StringTableMetaLanguage::RenderString port — callers own
/// keeping it to token-free templates (register row AD-81's scope note).
///
public string? ResolveTemplate(
uint tableId,
string key,
IReadOnlyDictionary variables)
{
ArgumentNullException.ThrowIfNull(key);
ArgumentNullException.ThrowIfNull(variables);
if (tableId == 0u)
return null;
if (!_tables.TryGetValue(tableId, out StringTable? table))
{
table = _dats.Get(tableId);
_tables[tableId] = table;
}
if (table is null
|| !table.Strings.TryGetValue(ComputeHash(key), out var entry)
|| entry.Strings.Count == 0)
return null;
var composed = new System.Text.StringBuilder();
for (int i = 0; i < entry.Strings.Count; i++)
{
composed.Append(entry.Strings[i].Value);
// Variables are stored as the pre-computed name hashes (the same
// compute_str_hash space PlayerVariable lives in). Each value is
// escaped on insert — retail's AddVariable_String @ 0x0042E6C0
// stores every variable through SetLiteralValue(escape=1)
// @ 0x0042C980 → EscapeString — so the final whole-string
// unescape below returns variable content verbatim while
// decoding the authored fragments' escapes.
if (i < entry.Variables.Count
&& variables.TryGetValue(entry.Variables[i], out string? value))
{
composed.Append(RetailStringEscapes.Escape(value));
}
}
// StringInfo::InqString @ 0x0042E490's unconditional tail, same as
// Resolve above: composed text decodes its escapes at the source.
return RetailStringEscapes.Unescape(composed.ToString());
}
///
/// Exact retail ELF-style string hash used for StringInfo keys.
/// Ported line-for-line from compute_str_hash @ 0x00413110.
///
public static uint ComputeHash(string value)
{
ArgumentNullException.ThrowIfNull(value);
uint result = 0u;
foreach (char c in value)
{
result = unchecked((result << 4) + (byte)c);
uint high = result & 0xF0000000u;
if (high != 0u)
result = ((high >> 24) ^ result) & 0x0FFFFFFFu;
}
return result == uint.MaxValue ? uint.MaxValue - 1u : result;
}
}