fix(ui): Campaign CT4 fix round — luminance text, verbatim title, PK PWD bits
Opus dual-lens review of ed652ed8 found 2 blockers + 5 should-fix. All applied.
BLOCKERS:
- Bind the luminance pair (0x100005C5/0x100005C6): caption "Luminance:"
(UTF-16, PE-byte-decoded from the gmStatManagementUI vftable-adjacent
data at @0x007c3dd4) and value "<available> / <maximum>" (narrow
"%s / %s" @0x007c3dcc) — both literals independently re-derived from the
raw acclient.exe bytes and confirmed byte-exact against the review's
claim. Numbers format through a new shared FormatXp helper
(.ToString("N0", InvariantCulture) — retail's ExperienceSystem::XPToString
equivalent), also now used by Total XP / XP-to-next-level (previously an
un-invariant bare "N0"). Hide path switched from Visible=false to
retail's own UIElement_Text::ClearAllText mechanism
(@0x004f0e31/@0x004f0e3c — empty LinesProvider, leave layout); each
LinesProvider re-reads data() on every draw, so no separate refresh call
is needed.
- CharacterIdentityText.StripLeadingArticle deleted: retail AppendText's
the resolved title VERBATIM (@0x004f0990); 26 real ACE CharacterTitle
entries begin with "The" and were being mangled. The dead
CharacterSheet.Race fallback is deleted alongside it — retail's
InqGenderHeritageDisplay creature-type argument is a hardcoded literal 0
(@0x004f08db), no producer exists.
SHOULD-FIX:
- PK line re-sourced: classifies off the live ClientObject.PublicWeenieBitfield
PWD bits (0x20 IsPK / 0x02000000 IsPKLite — ACCWeenieObject::IsPK/IsPKLite
@0x0058c8b0/@0x0058c8a0) instead of a bitwise test against raw
PropertyInt 134, which carries ACE's own PlayerKillerStatus enum bit
layout, not the PWD layout. PropertyInt 134 already drives the correct
bits via the existing PlayerKillerStatusBitfield.Apply; this is a
re-source, not new wiring. Deleted the 0x4|0x8 combined-flag test case,
which asserted a non-retail answer.
- Register AP-109 row: restores CT3's Titles-page narrowing paragraph
(CT4's edit had compressed it to a bare pointer phrase), corrects the
rank-prefix source to PropertyInt 0x1E (AllegianceRank) read live off
the qualities bundle — not RuntimeAllegianceState, which is a different
UI's (SocialAllegiancePageController) own documented substitute —
corrects the title-table size from an estimated 22 functions/~200
strings to the actual 17 functions/~170 strings (AllegianceSystem::GetTitle's
dispatch switch read directly), and downgrades the evidence claim.
Filed AP-235 for the gender/heritage hardcoded-table-vs-live-EnumMapper
mechanism divergence, pointing at the ALREADY-EXISTING
RetailDataIdResolver.Resolve helper as CT5's unification seam.
- CharacterPanelLiveDatTests.HeaderElements_AuthorExpectedFontsAndColors
extended with the luminance pair's own occurrence-count + font/color
pins, matching every other header id's pattern.
Also landed: an InstalledDat pin
(GenderHeritageDisplayNameTables_MatchTheRetailEnumMapperChain) proving
CharacterIdentityText.GenderDisplayName/HeritageGroupDisplayName match the
live retail EnumMapper chain (master map category 1 ->
ClientEnumToID[0x10000001]/[0x10000002] -> EnumMapper DIDs
0x2200000A/0x2200000B) byte-exact, including the two entries the review
flagged as unverified guesses (10 "Penumbraen", 12 "Olthoi" — both
correct). CharacterSheetProvider.BuildSheet's level read switched from a
GetInt+ContainsKey double lookup to one TryGetValue. Plan ledger's
test-provenance sentence corrected (Bind_HeaderElements_... predates CT4,
extended to cover PkStatusId).
Tests: CharacterStatControllerTests (verbatim title incl. "The Noob",
luminance content/gate, luminance text binding, extended
Bind_HeaderElements_... covering PkStatusId), CharacterSheetProviderTests
(PK status driven through ClientObjectTable.UpdateIntProperty instead of
a raw property write), CharacterPanelLiveDatTests (luminance pin, gender/
heritage EnumMapper pin). Full hermetic solution suite green under Release
(0 failures, 15 projects); InstalledDat pins green (197/197, excluding one
confirmed pre-existing unrelated failure — TowerAscentReplayTests, verified
to fail identically with these changes stashed out).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
ed652ed8ad
commit
e7e32409c2
9 changed files with 472 additions and 117 deletions
|
|
@ -12,47 +12,72 @@ namespace AcDream.App.UI.Layout;
|
|||
/// matched it.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <b>Name-line ruling (CT4, 2026-08-24).</b> Retail's NAME line
|
||||
/// <b>Name-line ruling (CT4, 2026-08-24; corrected at the CT4 fix round,
|
||||
/// 2026-08-25 — AP-109).</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. <see cref="AcDream.Runtime.Gameplay.RuntimeAllegianceState"/>
|
||||
/// (Campaign FA) DOES carry the local player's own rank
|
||||
/// (<c>ApplyUpdate</c>'s <c>_rank</c>, seeded by <c>0x0020
|
||||
/// AllegianceUpdate</c> — always the local tree), so the DATA half exists.
|
||||
/// The STRING half does not: <c>GetTitle</c> dispatches on heritage×gender
|
||||
/// into 22 separate functions (<c>GetAluvianMaleTitle @0x005b7bc0</c>,
|
||||
/// <c>GetAluvianFemaleTitle @0x005b7cd0</c>, … one per heritage/gender pair
|
||||
/// through Undead), each a rank-indexed switch over ~10 HARDCODED literal
|
||||
/// strings (Aluvian male: "Yeoman"/"Baronet"/"Baron"/"Reeve"/"Thane"/
|
||||
/// "Ealdor"/"Duke"/"Aetheling"/"King"/"High King" — verbatim from the
|
||||
/// decomp, not DAT-resolved, not guessed) — roughly 200 title strings
|
||||
/// total. That is not "reasonable size" for this slice on top of its other
|
||||
/// four items, so <see cref="CharacterStatController.Bind"/>'s Name label
|
||||
/// ships the PLAIN-NAME case only (matching the owner's own retail
|
||||
/// screenshot, a rankless character, and every current test character).
|
||||
/// The missing rank-prefix path is registered
|
||||
/// (<c>docs/architecture/retail-divergence-register.md</c>) rather than
|
||||
/// silently omitted.
|
||||
/// @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"/>
|
||||
/// already reads every other header property straight off
|
||||
/// <c>props.GetInt(...)</c>, so a future port reads
|
||||
/// <c>props.GetInt(0x1Eu)</c> directly instead. The STRING half is missing:
|
||||
/// <c>GetTitle</c>'s own dispatch switch (read directly, not estimated) has
|
||||
/// exactly 17 <c>Get*Title</c> functions, not 22 — Gearknight/Tumerok author
|
||||
/// only a MALE function (reused for both genders' dispatch branches) and
|
||||
/// Lugian only a FEMALE one (likewise reused both ways), so 11 heritages
|
||||
/// produce 17 functions; Olthoi/OlthoiAcid have none at all (the dispatch's
|
||||
/// own unsigned range check excludes heritage ids 12/13). Each function is a
|
||||
/// rank-indexed switch over ~10 HARDCODED literal strings (Aluvian male:
|
||||
/// "Yeoman"/"Baronet"/"Baron"/"Reeve"/"Thane"/"Ealdor"/"Duke"/"Aetheling"/
|
||||
/// "King"/"High King" — verbatim from the decomp, not DAT-resolved, not
|
||||
/// guessed) — roughly 170 title strings total. That is not "reasonable
|
||||
/// size" for one slice on top of its other work, so
|
||||
/// <see cref="CharacterStatController.Bind"/>'s Name label ships the
|
||||
/// PLAIN-NAME case only (matching the owner's own retail screenshot, a
|
||||
/// rankless character, and every current test character). The missing
|
||||
/// rank-prefix path is registered
|
||||
/// (<c>docs/architecture/retail-divergence-register.md</c>, AP-109) rather
|
||||
/// than silently omitted.
|
||||
/// </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)
|
||||
{
|
||||
string? heritage = !string.IsNullOrWhiteSpace(sheet.Heritage)
|
||||
? sheet.Heritage
|
||||
: sheet.Race;
|
||||
|
||||
string? title = StripLeadingArticle(sheet.Title);
|
||||
|
||||
if (string.IsNullOrWhiteSpace(sheet.Gender))
|
||||
return Join(heritage, title);
|
||||
return Join(sheet.Gender, heritage, title);
|
||||
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",
|
||||
|
|
@ -60,6 +85,19 @@ internal static class CharacterIdentityText
|
|||
_ => 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",
|
||||
|
|
@ -84,14 +122,4 @@ internal static class CharacterIdentityText
|
|||
.Where(p => !string.IsNullOrWhiteSpace(p))
|
||||
.Select(p => p!.Trim()));
|
||||
}
|
||||
|
||||
private static string? StripLeadingArticle(string? title)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(title)) return null;
|
||||
|
||||
string trimmed = title.Trim();
|
||||
return trimmed.StartsWith("the ", System.StringComparison.OrdinalIgnoreCase)
|
||||
? trimmed[4..]
|
||||
: trimmed;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue