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())); } }