The AS5 review verified the port exhaustively (170/170 strings, 20 dispatch arms, 17 bounds tests, both call sites) and found one docs-class defect: five code comments plus the plan slice text claimed AP-109 was RETIRED while the register correctly keeps the row ACTIVE-narrowed (CT4 FormatXp GetNumberFormatA approximation sliver survives). Comment-only edits; compile-checked; no behavior change. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
143 lines
6.8 KiB
C#
143 lines
6.8 KiB
C#
namespace AcDream.App.UI.Layout;
|
|
|
|
/// <summary>
|
|
/// 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);
|
|
/// <see cref="Join"/>'s existing <c>string.Join(" ", ...)</c> already
|
|
/// matched it.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// <b>Name-line ruling (CT4, 2026-08-24; CLOSED at Campaign AS slice AS5,
|
|
/// 2026-08-25 — narrows AP-109; its FormatXp sliver survives).</b> Retail's NAME line
|
|
/// (<c>AllegianceData::GetFullName @0x005b6950</c>) prefixes an allegiance
|
|
/// RANK title ("<RankTitle> <Name>", same space separator, PE-read
|
|
/// @data_794098) when <c>AllegianceSystem::GetTitle(rank, heritage, gender)
|
|
/// @0x005b8dd0</c> resolves one. The RANK value is PropertyInt <c>0x1E</c>
|
|
/// (<c>AllegianceRank</c>) read LIVE off the qualities bundle
|
|
/// (<c>CBaseQualities::InqInt(qualities, 0x1e)</c>) — NOT
|
|
/// <see cref="AcDream.Runtime.Gameplay.RuntimeAllegianceState"/>. That state
|
|
/// class carries a numerically-equivalent rank for a DIFFERENT UI
|
|
/// (<c>SocialAllegiancePageController</c>, which has no qualities-bundle
|
|
/// access of its own); <see cref="CharacterSheetProvider.BuildSheet"/>
|
|
/// reads every other header property straight off
|
|
/// <c>props.GetInt(...)</c>, and now the rank-prefix too. The 17-function
|
|
/// <c>Get*Title</c> 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
|
|
/// <see cref="AllegianceRankTitleTable"/>, whose class remarks carry the
|
|
/// full per-function address citations and the two independently-verified
|
|
/// decomp call sites. <see cref="CharacterStatController.Bind"/>'s Name
|
|
/// label now composes through
|
|
/// <see cref="AllegianceRankTitleTable.ComposeFullName"/> via
|
|
/// <see cref="CharacterSheetProvider.BuildSheet"/>.
|
|
/// </remarks>
|
|
internal static class CharacterIdentityText
|
|
{
|
|
public const uint GenderPropertyId = 0x71u;
|
|
public const uint HeritageGroupPropertyId = 0xBCu;
|
|
|
|
/// <summary>
|
|
/// CT4 fix round (2026-08-25, BLOCKER 2): retail's <c>AppendText</c> at
|
|
/// <c>@0x004f0990</c> appends the resolved <c>CharacterTitleTable</c>
|
|
/// string VERBATIM — no article stripping. 26 real ACE
|
|
/// <c>CharacterTitle</c> entries begin with "The" (e.g. "The Noob"), so
|
|
/// the former <c>StripLeadingArticle</c> call mangled every one of them.
|
|
/// Heritage also drops its <see cref="CharacterSheet.Race"/> fallback the
|
|
/// same round: <c>InqGenderHeritageDisplay</c>'s third argument
|
|
/// (creature type) is a hardcoded literal <c>0</c> at
|
|
/// <c>@0x004f08db</c>, not sourced from any producer — retail has no
|
|
/// "race" input to this line at all.
|
|
/// </summary>
|
|
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);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Retail: <c>AppraisalSystem::InqGenderDisplayName @0x005b47c0</c> →
|
|
/// <c>EnumMapper::GetString(0x10000001, gender, ...)</c> →
|
|
/// <c>DBObj::GetDIDByEnum</c> (master map category 1, EnumMapper DID
|
|
/// <c>0x2200000A</c>) — 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
|
|
/// <c>CharacterPanelLiveDatTests.GenderHeritageDisplayNameTables_MatchTheRetailEnumMapperChain</c>.
|
|
/// </summary>
|
|
public static string? GenderDisplayName(int gender) => gender switch
|
|
{
|
|
1 => "Male",
|
|
2 => "Female",
|
|
_ => null,
|
|
};
|
|
|
|
/// <summary>
|
|
/// Retail: <c>AppraisalSystem::InqHeritageGroupDisplayName @0x005b4710</c>
|
|
/// hardcodes ids 2/5/0xd to "Gharu'ndim"/"Umbraen"/"Olthoi", else falls
|
|
/// through to <c>EnumMapper::GetString(0x10000002, heritage, ...)</c> →
|
|
/// <c>DBObj::GetDIDByEnum</c> (master map category 1, EnumMapper DID
|
|
/// <c>0x2200000B</c>) — 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
|
|
/// <see cref="GenderDisplayName"/>'s sibling InstalledDat pin.
|
|
/// </summary>
|
|
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,
|
|
};
|
|
|
|
/// <summary>
|
|
/// Campaign AS slice AS2 (2026-08-25): examination window element
|
|
/// <c>0x10000150</c> (Heritage), <c>CharExamineUI::SetAppraiseInfo
|
|
/// @0x004B45F0</c> via <c>AppraisalSystem::InqGenderHeritageDisplay
|
|
/// @0x005B5AE0</c>. Unlike <see cref="StatHeaderLine"/> (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
|
|
/// <paramref name="creatureTypeFallback"/> (the assessed object's
|
|
/// creature-type display name, Int 2, resolved through the SAME
|
|
/// EnumMapper family <see cref="CreatureDisplayNameResolver"/> already
|
|
/// serves the monster view's own <c>0x1000014E</c> line) ONLY when
|
|
/// <paramref name="heritageGroup"/> is exactly 0, matching retail's own
|
|
/// integer comparison rather than "the lookup failed".
|
|
/// </summary>
|
|
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()));
|
|
}
|
|
}
|