namespace AcDream.App.UI.Layout;
///
/// Retail character identity display helpers for gmStatManagementUI.
/// Sources: gmStatManagementUI::UpdateCharacterInfo (0x004f0770) calls
/// AppraisalSystem::InqGenderHeritageDisplay(gender 0x71, heritage 0xBC, 0),
/// then — when CharacterTitleTable::GetCharacterTitleFromID(m_titleID)
/// resolves — AppendText(separator @data_794358) + AppendText(titleString).
/// Campaign CT slice CT4 (2026-08-24) PE-read RECOVERED the separator as a
/// single space " " (both here and for the allegiance-rank prefix below);
/// 's existing string.Join(" ", ...) already
/// matched it.
///
///
/// Name-line ruling (CT4, 2026-08-24; CLOSED at Campaign AS slice AS5,
/// 2026-08-25 — narrows AP-109; its FormatXp sliver survives). Retail's NAME line
/// (AllegianceData::GetFullName @0x005b6950) prefixes an allegiance
/// RANK title ("<RankTitle> <Name>", same space separator, PE-read
/// @data_794098) when AllegianceSystem::GetTitle(rank, heritage, gender)
/// @0x005b8dd0 resolves one. The RANK value is PropertyInt 0x1E
/// (AllegianceRank) read LIVE off the qualities bundle
/// (CBaseQualities::InqInt(qualities, 0x1e)) — NOT
/// . That state
/// class carries a numerically-equivalent rank for a DIFFERENT UI
/// (SocialAllegiancePageController, which has no qualities-bundle
/// access of its own);
/// reads every other header property straight off
/// props.GetInt(...), and now the rank-prefix too. The 17-function
/// Get*Title table (Gearknight/Tumerok author only a MALE function,
/// reused for both genders' dispatch branches; Lugian only a FEMALE one,
/// likewise reused both ways; Olthoi/OlthoiAcid excluded by the dispatch's
/// own unsigned range check) is ported verbatim at
/// , whose class remarks carry the
/// full per-function address citations and the two independently-verified
/// decomp call sites. 's Name
/// label now composes through
/// via
/// .
///
internal static class CharacterIdentityText
{
public const uint GenderPropertyId = 0x71u;
public const uint HeritageGroupPropertyId = 0xBCu;
///
/// CT4 fix round (2026-08-25, BLOCKER 2): retail's AppendText at
/// @0x004f0990 appends the resolved CharacterTitleTable
/// string VERBATIM — no article stripping. 26 real ACE
/// CharacterTitle entries begin with "The" (e.g. "The Noob"), so
/// the former StripLeadingArticle call mangled every one of them.
/// Heritage also drops its fallback the
/// same round: InqGenderHeritageDisplay's third argument
/// (creature type) is a hardcoded literal 0 at
/// @0x004f08db, not sourced from any producer — retail has no
/// "race" input to this line at all.
///
public static string StatHeaderLine(CharacterSheet sheet)
{
if (string.IsNullOrWhiteSpace(sheet.Gender))
return Join(sheet.Heritage, sheet.Title);
return Join(sheet.Gender, sheet.Heritage, sheet.Title);
}
///
/// Retail: AppraisalSystem::InqGenderDisplayName @0x005b47c0 →
/// EnumMapper::GetString(0x10000001, gender, ...) →
/// DBObj::GetDIDByEnum (master map category 1, EnumMapper DID
/// 0x2200000A) — a LIVE DAT read. This table is a hardcoded C#
/// mechanism substitute (register row AP-235); its content is verified
/// byte-exact against the live EnumMapper by the InstalledDat pin
/// CharacterPanelLiveDatTests.GenderHeritageDisplayNameTables_MatchTheRetailEnumMapperChain.
///
public static string? GenderDisplayName(int gender) => gender switch
{
1 => "Male",
2 => "Female",
_ => null,
};
///
/// Retail: AppraisalSystem::InqHeritageGroupDisplayName @0x005b4710
/// hardcodes ids 2/5/0xd to "Gharu'ndim"/"Umbraen"/"Olthoi", else falls
/// through to EnumMapper::GetString(0x10000002, heritage, ...) →
/// DBObj::GetDIDByEnum (master map category 1, EnumMapper DID
/// 0x2200000B) — a LIVE DAT read whose raw entries are the
/// internal names ("Gharundim", "Shadowbound", "OlthoiAcid" for those
/// same three ids). This table is a hardcoded C# mechanism substitute
/// (register row AP-235); every entry, including the ones the CT4 review
/// flagged as unverified guesses (10 "Penumbraen", 12 "Olthoi"), is
/// verified byte-exact against the live EnumMapper chain by
/// 's sibling InstalledDat pin.
///
public static string? HeritageGroupDisplayName(int heritageGroup) => heritageGroup switch
{
1 => "Aluvian",
2 => "Gharu'ndim",
3 => "Sho",
4 => "Viamontian",
5 => "Umbraen",
6 => "Gearknight",
7 => "Tumerok",
8 => "Lugian",
9 => "Empyrean",
10 => "Penumbraen",
11 => "Undead",
12 => "Olthoi",
13 => "Olthoi",
_ => null,
};
///
/// Campaign AS slice AS2 (2026-08-25): examination window element
/// 0x10000150 (Heritage), CharExamineUI::SetAppraiseInfo
/// @0x004B45F0 via AppraisalSystem::InqGenderHeritageDisplay
/// @0x005B5AE0. Unlike (the
/// stat-management panel's header, which never appends a title and has
/// no creature-type fallback), this composes ONLY
/// "<Gender> <Heritage>" — falling back to
/// (the assessed object's
/// creature-type display name, Int 2, resolved through the SAME
/// EnumMapper family already
/// serves the monster view's own 0x1000014E line) ONLY when
/// is exactly 0, matching retail's own
/// integer comparison rather than "the lookup failed".
///
public static string GenderHeritageDisplay(
int gender,
int heritageGroup,
string? creatureTypeFallback)
{
string? heritage = heritageGroup == 0
? creatureTypeFallback
: HeritageGroupDisplayName(heritageGroup);
return Join(GenderDisplayName(gender), heritage);
}
private static string Join(params string?[] parts)
{
return string.Join(" ", parts
.Where(p => !string.IsNullOrWhiteSpace(p))
.Select(p => p!.Trim()));
}
}