using System.Numerics;
using AcDream.App.UI.Layout;
using AcDream.Content;
using DatReaderWriter;
namespace AcDream.App.Tests.UI.Layout;
///
/// Campaign CT slice CT1 (2026-08-24) pins: the DAT ground truth for the
/// character panel (LayoutDesc 0x2100002E) that slices CT2–CT6 build
/// against. Findings + raw probe excerpts live in
/// docs/research/2026-08-24-campaign-ct-dat-ground-truth.md. Follows
/// the durable-pin pattern of /
/// — assertions, not dumps — so a DAT
/// revision or importer regression that silently changes any of these
/// authored facts fails loudly instead of drifting unnoticed into CT2–CT6.
///
/// Decomp anchors: header elements are bound by
/// gmStatManagementUI::PostInit @0x004EFD90; the Titles page by
/// gmCharacterTitleUI::PostInit @0x0049A610.
///
[Trait("Lane", "InstalledDat")]
public sealed class CharacterPanelLiveDatTests
{
private static string DatDirectory =>
Environment.GetEnvironmentVariable("ACDREAM_DAT_DIR")
?? Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.UserProfile),
"Documents", "Asheron's Call");
private static IEnumerable Flatten(ElementInfo e)
{
yield return e;
foreach (var c in e.Children)
foreach (var d in Flatten(c))
yield return d;
}
///
/// Header identity + level elements, bound by
/// gmStatManagementUI::PostInit @0x004EFD90. Pins item 4 (the PK
/// line is authored pure white — the CT4 bug is in the controller's
/// runtime color choice, NOT a DAT-reading gap) and item 5 (the level
/// color is a pale gold with an authored outline, not
/// CharacterStatController's current deeper-orange
/// Gold constant). Both header occurrences (Attributes-page and
/// Skills-page chains) are asserted identical — the harmless
/// "duplicated stat-management branches" quirk documented on
/// PrepareSkillScrollbar.
///
[InstalledDatFact]
public void HeaderElements_AuthorExpectedFontsAndColors()
{
using var dats = new DatCollection(DatDirectory, DatReaderWriter.Options.DatAccessType.Read);
ElementInfo? tree = LayoutImporter.ImportInfos(dats, 0x2100002Eu);
Assert.NotNull(tree);
var nameOccurrences = Flatten(tree!).Where(e => e.Id == CharacterStatController.NameId).ToList();
Assert.Equal(2, nameOccurrences.Count); // Attributes-page + Skills-page duplicate chains
foreach (var name in nameOccurrences)
{
Assert.Equal(0x40000001u, name.FontDid);
Assert.Equal(Vector4.One, name.FontColor);
}
// CT1 fix round: hoisted to .ToList() + an explicit count assertion.
// A bare `foreach (x in seq.Where(...))` with no count check passes
// vacuously if the importer ever returns an empty sequence — it
// proves nothing about the two Attributes/Skills duplicate chains
// actually being present.
var heritageOccurrences = Flatten(tree!).Where(e => e.Id == CharacterStatController.HeritageId).ToList();
Assert.Equal(2, heritageOccurrences.Count);
foreach (var heritage in heritageOccurrences)
{
Assert.Equal(0x40000002u, heritage.FontDid);
Assert.Equal(Vector4.One, heritage.FontColor);
Assert.Equal(5, heritage.MarginLeft);
Assert.Equal(5, heritage.MarginRight);
}
// Item 4: PK status line is authored PURE WHITE.
var pkOccurrences = Flatten(tree!).Where(e => e.Id == CharacterStatController.PkStatusId).ToList();
Assert.Equal(2, pkOccurrences.Count);
foreach (var pk in pkOccurrences)
{
Assert.Equal(0x40000002u, pk.FontDid);
Assert.Equal(Vector4.One, pk.FontColor);
}
// Item 5: level color is a pale gold (~RGB 255/242/127) WITH an
// authored outline — CharacterStatController.Gold (1, 0.82, 0.36, 1)
// with no outline is a divergent hand-picked runtime color.
var levelOccurrences = Flatten(tree!).Where(e => e.Id == CharacterStatController.LevelId).ToList();
Assert.Equal(2, levelOccurrences.Count);
foreach (var level in levelOccurrences)
{
Assert.Equal(0x40000010u, level.FontDid);
Assert.True(level.Outline);
Assert.NotNull(level.FontColor);
Assert.Equal(1f, level.FontColor!.Value.X, precision: 3);
Assert.Equal(0.949f, level.FontColor!.Value.Y, precision: 2);
Assert.Equal(0.498f, level.FontColor!.Value.Z, precision: 2);
}
var xpLabelOccurrences = Flatten(tree!).Where(e => e.Id == CharacterStatController.XpNextLabelId).ToList();
Assert.Equal(2, xpLabelOccurrences.Count);
foreach (var xpLabel in xpLabelOccurrences)
Assert.Equal(0x40000000u, xpLabel.FontDid);
var xpValueOccurrences = Flatten(tree!).Where(e => e.Id == CharacterStatController.XpNextValueId).ToList();
Assert.Equal(2, xpValueOccurrences.Count);
foreach (var xpValue in xpValueOccurrences)
Assert.Equal(0x40000000u, xpValue.FontDid);
// CT4 fix round (2026-08-25, SHOULD-FIX 5): the luminance pair's own
// binding-seam pin — same occurrence-count pattern as every other
// header id above (Attributes-page + Skills-page duplicate chains).
// Font/color ground truth from
// docs/research/2026-08-24-campaign-ct-dat-ground-truth.md: both
// elements author font 0x40000000, pure white, no outline.
var luminanceLabelOccurrences = Flatten(tree!).Where(e => e.Id == CharacterStatController.LuminanceLabelId).ToList();
Assert.Equal(2, luminanceLabelOccurrences.Count);
foreach (var luminanceLabel in luminanceLabelOccurrences)
{
Assert.Equal(0x40000000u, luminanceLabel.FontDid);
Assert.Equal(Vector4.One, luminanceLabel.FontColor);
}
var luminanceValueOccurrences = Flatten(tree!).Where(e => e.Id == CharacterStatController.LuminanceValueId).ToList();
Assert.Equal(2, luminanceValueOccurrences.Count);
foreach (var luminanceValue in luminanceValueOccurrences)
{
Assert.Equal(0x40000000u, luminanceValue.FontDid);
Assert.Equal(Vector4.One, luminanceValue.FontColor);
}
}
///
/// The stat ListBox (0x1000023D) authors a five-entry template
/// list in LayoutDesc 0x21000045: one shared icon+name+value data
/// row (0x10000248) plus four skill section-header captions
/// (0x10000249..0x1000024C). Pins the roster + the ListBox's own
/// authored scrollbar linkage.
///
[InstalledDatFact]
public void StatListBox_AuthorsFiveRowTemplatesInSharedLayout()
{
using var dats = new DatCollection(DatDirectory, DatReaderWriter.Options.DatAccessType.Read);
ElementInfo? tree = LayoutImporter.ImportInfos(dats, 0x2100002Eu);
Assert.NotNull(tree);
// Duplicated once per Attributes-page/Skills-page chain, same as the
// header block — both copies are asserted identical.
var listBoxes = Flatten(tree!).Where(e => e.Id == CharacterStatController.ListBoxId).ToList();
Assert.Equal(2, listBoxes.Count);
uint[] expected =
{
0x10000248u, 0x10000249u, 0x1000024Au, 0x1000024Bu, 0x1000024Cu,
};
foreach (ElementInfo listBox in listBoxes)
{
// CT1 fix round: authored ListBox rect — CT5's row alignment
// and CT6's resize/scrollbar contract both depend on this.
Assert.Equal(0f, listBox.X);
Assert.Equal(112f, listBox.Y);
Assert.Equal(300f, listBox.Width);
Assert.Equal(160f, listBox.Height);
Assert.Equal(CharacterStatController.ListScrollbarId, listBox.ScrollbarElementId);
Assert.Equal(5, listBox.TemplateList.Count);
foreach (uint id in expected)
{
Assert.Contains(
listBox.TemplateList,
t => t.TemplateLayoutId == 0x21000045u && t.TemplateElementId == id);
}
}
// CT1 fix round: the ListBox's own authored scrollbar rect — the
// "always reserved" gutter CT5/CT6 both depend on.
var scrollbars = Flatten(tree!).Where(e => e.Id == CharacterStatController.ListScrollbarId).ToList();
Assert.Equal(2, scrollbars.Count);
foreach (ElementInfo scrollbar in scrollbars)
{
Assert.Equal(281f, scrollbar.X);
Assert.Equal(16f, scrollbar.Width);
Assert.Equal(160f, scrollbar.Height);
}
}
///
/// The shared attribute/skill data-row template (0x10000248 in
/// LayoutDesc 0x21000045) — the icon/name/value geometry CT5
/// implements against. NOTE: this element is intentionally unreachable
/// via 's
/// whole-layout overload (the #375 same-layout template-list skip
/// filter) — the targeted single-root overload
/// (ImportInfos(dats, layoutId, elementId)) is required, the same
/// seam UiTemplateListBox's TemplateResolver already uses.
///
[InstalledDatFact]
public void AttributeRowTemplate_IconIsFlushLeftTwentyPixels_NameAndValueAreFixedColumns()
{
using var dats = new DatCollection(DatDirectory, DatReaderWriter.Options.DatAccessType.Read);
ElementInfo? row = LayoutImporter.ImportInfos(dats, 0x21000045u, 0x10000248u);
Assert.NotNull(row);
Assert.Equal(282f, row!.Width);
Assert.Equal(20f, row.Height);
Assert.Equal(0x06004CC2u, row.StateMedia["Normal"].File);
// Row-template Highlight sprite (0x06000F93) — CharacterStatController's
// current RowHighlightSprite private constant is 0x06001397. CT1's
// fix round upgraded this from a flagged divergence to a SEALED
// VERDICT (gmAttributeUI::UpdateSelection SetState(6) ->
// InfoRegion::SetState on this exact template); 0x06001397 belongs
// to the spellbook row's separate selected-overlay mechanism. CT5
// fixes RowHighlightSprite for the stat rows only; see the research
// doc's "Corrections to the plan" §3.
Assert.Equal(0x06000F93u, row.StateMedia["Highlight"].File);
ElementInfo icon = Assert.Single(row.Children, c => c.Id == 0x10000129u);
Assert.Equal(0f, icon.X);
Assert.Equal(0f, icon.Y);
Assert.Equal(20f, icon.Width);
Assert.Equal(20f, icon.Height);
ElementInfo name = Assert.Single(row.Children, c => c.Id == 0x1000012Au);
Assert.Equal(25f, name.X);
Assert.Equal(150f, name.Width);
Assert.Equal(HJustify.Left, name.HJustify);
Assert.Equal(0x40000001u, name.FontDid);
ElementInfo value = Assert.Single(row.Children, c => c.Id == 0x1000012Bu);
Assert.Equal(175f, value.X);
Assert.Equal(100f, value.Width);
Assert.Equal(HJustify.Right, value.HJustify);
Assert.Equal(0x40000001u, value.FontDid);
// Item 2: 7px gap between the value's right edge (275) and the row's
// own right edge (282) — the authored margin the owner reported.
Assert.Equal(7f, row.Width - (value.X + value.Width));
}
///
/// The four skill section-header caption templates already match
/// CharacterStatController's existing sprite constants exactly —
/// a regression guard, not a bug pin (unlike the data-row template
/// above).
///
[InstalledDatFact]
public void SkillSectionHeaderTemplates_MatchExistingSpriteConstants()
{
using var dats = new DatCollection(DatDirectory, DatReaderWriter.Options.DatAccessType.Read);
(uint elementId, uint expectedSprite)[] headers =
{
(0x10000249u, 0x06000F90u), // SkillHeaderSpecializedSprite
(0x1000024Au, 0x06000F86u), // SkillHeaderTrainedSprite
(0x1000024Bu, 0x06000F98u), // SkillHeaderUntrainedSprite
(0x1000024Cu, 0x06000F89u), // SkillHeaderUnusableSprite
};
foreach (var (elementId, expectedSprite) in headers)
{
ElementInfo? header = LayoutImporter.ImportInfos(dats, 0x21000045u, elementId);
Assert.NotNull(header);
Assert.Equal(280f, header!.Width);
Assert.Equal(20f, header.Height);
Assert.Equal(expectedSprite, header.StateMedia[""].File);
}
}
///
/// The Titles page (gmCharacterTitleUI::PostInit @0x0049A610)
/// element roster: display-title text 0x1000052F, title ListBox
/// 0x10000532 (with its scrollbar linkage + row-template
/// reference), "Set as Display Title" button 0x10000535 (with the
/// authored Ghosted default state matching the ghost-when-current
/// contract).
///
[InstalledDatFact]
public void TitlesPage_ElementRosterExists()
{
using var dats = new DatCollection(DatDirectory, DatReaderWriter.Options.DatAccessType.Read);
ElementInfo? tree = LayoutImporter.ImportInfos(dats, 0x2100002Eu);
Assert.NotNull(tree);
ElementInfo page = Assert.Single(Flatten(tree!), e => e.Id == 0x10000539u);
Assert.Equal(300f, page.Width);
Assert.Equal(575f, page.Height);
ElementInfo displayTitle = Assert.Single(page.Children, c => c.Id == 0x1000052Fu);
Assert.Equal(0x40000001u, displayTitle.FontDid);
Assert.Equal(HJustify.Center, displayTitle.HJustify);
ElementInfo listBox = Assert.Single(page.Children, c => c.Id == 0x10000532u);
Assert.Equal(5u, listBox.Type); // Type-5 ListBox
Assert.Equal(0x10000533u, listBox.ScrollbarElementId);
UiTemplateListEntry template = Assert.Single(listBox.TemplateList);
Assert.Equal(0x2100005Eu, template.TemplateLayoutId);
Assert.Equal(0x10000536u, template.TemplateElementId);
ElementInfo setDisplayButton = Assert.Single(page.Children, c => c.Id == 0x10000535u);
Assert.Equal(1u, setDisplayButton.Type); // Type-1 button
Assert.Equal(65, setDisplayButton.MinWidth);
Assert.Equal("Ghosted", setDisplayButton.DefaultStateName);
}
///
/// The title row template (0x10000536 in LayoutDesc
/// 0x2100005E) — a single-line text row, no icon column, 24px
/// tall. Same targeted-root-overload requirement as the stat row
/// templates.
///
[InstalledDatFact]
public void TitleRowTemplate_IsSingleLineTextRowWithNoIconColumn()
{
using var dats = new DatCollection(DatDirectory, DatReaderWriter.Options.DatAccessType.Read);
ElementInfo? row = LayoutImporter.ImportInfos(dats, 0x2100005Eu, 0x10000536u);
Assert.NotNull(row);
Assert.Equal(270f, row!.Width);
Assert.Equal(24f, row.Height);
// DirectState (the "" key), not a named "Normal" state.
Assert.Equal(0x06004CCAu, row.StateMedia[""].File);
Assert.Equal(0x06001AAFu, row.StateMedia["Highlight"].File);
ElementInfo text = Assert.Single(row.Children);
Assert.Equal(0x10000537u, text.Id);
Assert.Equal(270f, text.Width);
Assert.Equal(24f, text.Height);
Assert.Equal(HJustify.Left, text.HJustify);
Assert.Equal(6, text.MarginLeft);
Assert.Equal(6, text.MarginRight);
Assert.Equal(0x40000001u, text.FontDid);
}
///
/// Item 3/7 ground truth: the character window's imported root
/// (0x10000227, the Type-8 TabControl ImportInfos returns
/// and RetailUiRuntime.MountCharacter mounts) authors NO
/// MinWidth/MinHeight/MaxWidth/MaxHeight — unlike e.g. the side-vitals
/// window (LayoutDesc 0x21000075) whose root DOES author
/// 0x3C..0x3F directly. Pinned so CT6 doesn't waste a cycle assuming a
/// simple "read the DAT property" fix is available for this window;
/// see the research doc's "Corrections to the plan" §1.
///
[InstalledDatFact]
public void CharacterWindowRoot_AuthorsNoSizeConstraints()
{
using var dats = new DatCollection(DatDirectory, DatReaderWriter.Options.DatAccessType.Read);
ElementInfo? tree = LayoutImporter.ImportInfos(dats, 0x2100002Eu);
Assert.NotNull(tree);
Assert.Equal(0x10000227u, tree!.Id);
Assert.Equal(8u, tree.Type); // Type-8 TabControl
Assert.Null(tree.MinWidth);
Assert.Null(tree.MinHeight);
Assert.Null(tree.MaxWidth);
Assert.Null(tree.MaxHeight);
}
///
/// Comparison point for the pin above: the chat window's root DOES
/// author explicit size constraints, because unlike the character
/// panel's content-only layout, 0x2100006F's root IS a
/// self-contained window element (its own dragbar/border/resize-grip
/// children).
///
[InstalledDatFact]
public void ChatWindowRoot_AuthorsExplicitSizeConstraints()
{
using var dats = new DatCollection(DatDirectory, DatReaderWriter.Options.DatAccessType.Read);
ElementInfo? tree = LayoutImporter.ImportInfos(dats, 0x2100006Fu);
Assert.NotNull(tree);
Assert.Equal(0x10000600u, tree!.Id);
Assert.Equal(300, tree.MinWidth);
Assert.Equal(100, tree.MinHeight);
Assert.Equal(2000, tree.MaxWidth);
Assert.Equal(2000, tree.MaxHeight);
}
///
/// The title-string table chain
/// (CharacterTitleTable::GetCharacterTitleFromID @0x005c6ed0):
/// title id → EnumMapper(0x22000041) canonical name →
/// compute_str_hash → StringTable(0x2300000E) localized
/// text. Both DIDs are resolved via the two-level
/// DBObj::GetDIDByEnum master-map chain (master map
/// 0x25000000 → category map → target DID) — see the research
/// doc §5 for the full derivation. Verified end to end against ACE's
/// CharacterTitle.WarMage = 13.
///
[InstalledDatFact]
public void TitleStringTable_ResolvesWarMageEndToEnd()
{
using var dats = new DatCollection(DatDirectory, DatReaderWriter.Options.DatAccessType.Read);
bool gotMapper = dats.Portal.TryGet(
0x22000041u, out var titleEnumMapper);
Assert.True(gotMapper);
Assert.NotNull(titleEnumMapper);
string raw = titleEnumMapper!.IdToStringMap[13u].ToString();
Assert.Equal("ID_CharacterTitle_War_Mage", raw);
uint hash = DatStringResolver.ComputeHash(raw);
Assert.Equal(0x0543AF05u, hash);
var resolver = new DatStringResolver(dats);
string? resolved = resolver.Resolve(0x2300000Eu, hash);
Assert.Equal("War Mage", resolved);
}
///
/// CT4 fix round item 6: retail resolves gender via
/// AppraisalSystem::InqGenderDisplayName @0x005b47c0 and heritage
/// via InqHeritageGroupDisplayName @0x005b4710, both through the
/// STATIC EnumMapper::GetString(uint32_t enumValue, uint32_t queryId,
/// PStringBase<char>*) @0x0041ac40 overload, which itself calls
/// DBObj::GetDIDByEnum(&did, enumValue, 1) — master map
/// (0x25000000) category-1 sub-map (0x25000001), then
/// ClientEnumToID[0x10000001] (gender) / [0x10000002]
/// (heritage) resolve to EnumMapper DIDs 0x2200000A /
/// 0x2200000B. Both mappers' IdToStringMap entries are used
/// VERBATIM as final display text (no further StringTable hash step, per
/// the decomp) EXCEPT heritage ids 2/5/0xd, which retail hardcodes to
/// "Gharu'ndim"/"Umbraen"/"Olthoi" instead of the raw
/// "Gharundim"/"Shadowbound"/"OlthoiAcid" internal names. This pin proves
/// every /
/// table
/// entry matches that exact algorithm against the live installed DAT —
/// catching a wrong guess (none found: ids 10 "Penumbraen" and 12
/// "Olthoi", flagged as unverified guesses in the CT4 review, both come
/// back byte-exact). See the register's CT4 mechanism-divergence row for
/// why the tables stay hardcoded rather than reading this chain live.
///
[InstalledDatFact]
public void GenderHeritageDisplayNameTables_MatchTheRetailEnumMapperChain()
{
using var dats = new DatCollection(DatDirectory, DatReaderWriter.Options.DatAccessType.Read);
bool gotMaster = dats.Portal.TryGet(
(uint)dats.Portal.Header.MasterMapId, out var master);
Assert.True(gotMaster);
Assert.NotNull(master);
Assert.True(master!.ClientEnumToID.TryGetValue(1u, out uint categoryDid));
bool gotCategoryMap = dats.Portal.TryGet(categoryDid, out var categoryMap);
Assert.True(gotCategoryMap);
Assert.NotNull(categoryMap);
Assert.True(categoryMap!.ClientEnumToID.TryGetValue(0x10000001u, out uint genderDid));
Assert.Equal(0x2200000Au, genderDid);
Assert.True(dats.Portal.TryGet(genderDid, out var genderMapper));
Assert.NotNull(genderMapper);
foreach (var (id, raw) in genderMapper!.IdToStringMap)
{
string? expected = raw.Value == "Invalid" ? null : raw.Value;
Assert.Equal(expected, CharacterIdentityText.GenderDisplayName((int)id));
}
Assert.True(categoryMap.ClientEnumToID.TryGetValue(0x10000002u, out uint heritageDid));
Assert.Equal(0x2200000Bu, heritageDid);
Assert.True(dats.Portal.TryGet(heritageDid, out var heritageMapper));
Assert.NotNull(heritageMapper);
// AppraisalSystem::InqHeritageGroupDisplayName's three hardcoded
// overrides (@0x005b4718/0x005b4732/0x005b474c) — applied ahead of
// the raw EnumMapper text, exactly like the retail branch order.
var overrides = new Dictionary
{
[2u] = "Gharu'ndim",
[5u] = "Umbraen",
[0xDu] = "Olthoi",
};
foreach (var (id, raw) in heritageMapper!.IdToStringMap)
{
string? expected = overrides.TryGetValue(id, out string? overridden)
? overridden
: raw.Value == "Invalid" ? null : raw.Value;
Assert.Equal(expected, CharacterIdentityText.HeritageGroupDisplayName((int)id));
}
}
///
/// Campaign CT slice CT4: gmStatManagementUI::UpdatePKStatus
/// (0x004f00a0) resolves its three-way PK status text through
/// StringTable 0x23000001 by key (the same compute_str_hash
/// mechanism ChatWindowController's chat labels already use — no
/// EnumMapper indirection needed here, unlike the title chain above).
/// Pins the exact authored strings CharacterSheetProvider.PkStatusText
/// resolves against, discovered by a live probe against the installed
/// DAT set (not guessed): "Player Killer" / "Player Killer Lite" /
/// "Non-Player Killer".
///
[InstalledDatFact]
public void PkStatusKeys_ResolveExpectedAuthoredStrings()
{
using var dats = new DatCollection(DatDirectory, DatReaderWriter.Options.DatAccessType.Read);
var resolver = new DatStringResolver(dats);
string? pk = resolver.Resolve(0x23000001u,
DatStringResolver.ComputeHash("ID_StatManagement_Header_PKStatus_PK"));
string? pkLite = resolver.Resolve(0x23000001u,
DatStringResolver.ComputeHash("ID_StatManagement_Header_PKStatus_PKL"));
string? npk = resolver.Resolve(0x23000001u,
DatStringResolver.ComputeHash("ID_StatManagement_Header_PKStatus_NPK"));
Assert.Equal("Player Killer", pk);
Assert.Equal("Player Killer Lite", pkLite);
Assert.Equal("Non-Player Killer", npk);
}
///
/// Campaign CT slice CT5 (AP-235): gmAttributeUI::PostInit @0x0049DB70
/// resolves each row's icon DID via DBObj::GetDIDByEnum(statEnum, category
/// 0x10000002) for the 6 primary attributes (statId order 1,2,4,3,5,6 —
/// matching CharacterStatController.AttrRows' authored display order)
/// and DBObj::GetDIDByEnum(vitalEnum, category 0x10000003) for the 3
/// vitals' Attribute2ndInfoRegion icon (verified live: the max-vital
/// enum ids 1/3/5 that CharacterStatController.VitalRows already
/// stores resolve to the SAME DID as the current-vital ids 2/4/6 retail's
/// own decomp literally passes, so either works). Both categories route
/// through the shared RetailDataIdResolver.Resolve master-map ->
/// category-map -> value chain — this pin proves every hardcoded fallback
/// DID in AttrRows/VitalRows already matches the live-resolved
/// value byte-exact, the same "regression guard, not a bug pin" pattern as
/// .
///
[InstalledDatFact]
public void AttributeAndVitalIconDids_MatchTheRetailEnumMapperChain()
{
using var dats = new DatCollection(DatDirectory, DatReaderWriter.Options.DatAccessType.Read);
const uint attributeIconCategory = 0x10000002u;
(uint statId, uint expectedDid)[] attributes =
{
(1u, 0x060002C8u), // Strength
(2u, 0x060002C4u), // Endurance
(4u, 0x060002C9u), // Coordination
(3u, 0x060002C6u), // Quickness
(5u, 0x060002C5u), // Focus
(6u, 0x060002C7u), // Self
};
foreach (var (statId, expectedDid) in attributes)
{
uint resolved = RetailDataIdResolver.Resolve(dats, statId, attributeIconCategory);
Assert.Equal(expectedDid, resolved);
}
const uint vitalIconCategory = 0x10000003u;
(uint maxStatId, uint expectedDid)[] vitals =
{
(1u, 0x06004C3Bu), // Health
(3u, 0x06004C3Cu), // Stamina
(5u, 0x06004C3Du), // Mana
};
foreach (var (maxStatId, expectedDid) in vitals)
{
uint resolved = RetailDataIdResolver.Resolve(dats, maxStatId, vitalIconCategory);
Assert.Equal(expectedDid, resolved);
}
}
}