feat(ui): Campaign AS AS4 — society/allegiance/fellowship + configurable extras (G6/G7/G8)
Ports the last three retail extras-list row families from CharExamineUI:: SetAppraiseInfo @0x004B45F0 into CreatureAppraisalRows.BuildExtra, closing the character-path extras list end to end (AS2 header + AS3 armor-level/ ratings/legend + AS4 here). All rows are CHARACTER-path only; the monster path (CreatureExamineUI::SetAppraiseInfo @0x004B3FF0) is unaffected and pinned by a controller-level regression test. Society row (gap G6, @0x004b49a1-@0x004b4c24): gated on PropertyInt 281 (Faction1Bits) being PRESENT — a literal reading of InqInt's found/not- found return, not the ground-truth doc's informal "!= 0" value test. Bit-priority if/else-if chain (Celestial Hand 0x1 -> Eldrytch Web 0x2 -> "???" when Radiant Blood's 0x4 bit is ALSO clear -> else Radiant Blood) comes straight off the decompiled branches. Rank-band suffix boundaries (1-100 Initiate / 101-300 Adept / 301-600 Knight / 601-1000 Lord / 1001-1500 Master, no suffix outside every band) read directly off the decomp's inclusive range checks @0x004b4ab9-@0x004b4b92 and match the ground-truth table exactly. Color rule: green when the LOCAL player shares the target's selected bit (checked first, so extra local bits don't override a match), red when local has a different bit but not the target's, normal when local has no society bits; the unrecognized "???" branch never gets a color (retail's ebx_3 stays at its zero initializer). The local player's own Faction1Bits comes from a new pure `localFactionBits` parameter on BuildExtra — the composer never reads state directly. Monarch/Patron/Followers cascade (gap G7, @0x004b4d97-@0x004b4f54): gated by the caller on AllegianceRank (Int 30) >= 1, the SAME InqInt read AS2's header AllegianceName binding already consumes. Four arms in retail's exact order: MonarchsTitle (Str 21) absent -> "Alleg. Monarch:" + clamped "%d Follower"/"%d Followers" (Int 35, singular only at exactly 1); present + PatronsTitle (Str 35) absent -> "Monarch:" only; both present and ordinally equal -> one "Monarch/Patron:" row; both present and different -> "Monarch:" then "Patron:". Configurable extras (gap G8, @0x004b58be-@0x004b5c4d): Fellowship (Str 10), Arrived in Dereth (Str 43 DateOfBirth), Time in Dereth (Int 125 Age via the ALREADY-PORTED RetailDurationText.Format — the same ClientUISystem::DeltaTimeToString @0x00565E10 the decomp calls at @0x004b59e0, reused rather than re-ported), Chess Rank (Int 181), Fishing Skill (Int 192), Deaths (Int 43 NumDeaths, <= 0 -> "Has never died" with the SAME "Deaths:" label, verified in the decomp), Titles Earned (Int 262). Each row gates independently on its own property being PRESENT (server already strips these per the target's visibility options per ground truth §3) — no client-side option or success gating added. No spacers separate these seven rows, matching the decomp exactly. Seam: AppraisalUiController takes a new `Func<int> _localFactionBits` dependency (per-call, never captured once — the secure-trade deferred-Func lesson), invoked only on the character path. AppraisalRuntimeBindings gained `LocalFactionBits`, wired in InteractionRetainedUiComposition from `d.Character.LocalPlayer.Properties.GetInt(281)` — the SAME LocalPlayerState instance CharacterSheetProvider already reads from, no new state path. AS3 NIT 11: CharacterResponse_CombatRefreshRetainsArmorLevelRows now applies a SECOND response with different armor-level values (proving the refresh re-renders from fresh data) then a THIRD with armorLevels: null (proving it clears). AS3 NIT 12: BuildExtra's XML doc now documents the full authored row order with the @0x004b5d7d legend anchor. The test Parsed() helper now ORs IdentifyResponseFlags.ArmorLevels into Flags whenever armorLevels is supplied (realism sub-nit), applying uniformly to every existing armor-level test in the file. Ground-truth doc imprecisions found while verifying against the decomp directly: (1) the Society gate is presence-of-property, not value != 0 — InqInt's return is a found/not-found bool, the summary's "!= 0" phrasing describes the common case but not the literal branch condition; (2) the Society color rule's bit-priority (same-bit match checked before the other-bits check) wasn't spelled out in the summary table, only "same/ different/none" — confirmed exact by reading all three branches (@0x004b49fd/@0x004b4a49/@0x004b4a8b). No other disagreements found. Register: docs/architecture/retail-divergence-register.md row AP-110 retires the "exhaustive character detail regions" clause from its still-lacks list with a dated 2026-08-25 narrowing note (AS2+AS3+AS4 together closed the character-path extras list); the row's other residuals (item-object preview, effective shield projection, cooldown- remaining, augmentation-cost StringInfo, creature FontInfo-list selection) are untouched. Tests: 51 new rows-level tests in CreatureAppraisalRowsTests.cs (society gate/bit-priority/band-boundaries/color-vs-local-faction, the full allegiance cascade incl. follower singular/plural/clamp, each configurable extra present/absent, "Has never died", monster-path regression, one complete ordering-pin snapshot) plus 3 new controller- level tests in AppraisalUiControllerTests.cs through the real LayoutDesc/ template binding (localFactionBits seam wired end to end, its default fallback, and the strengthened monster-path regression). Hermetic AcDream.App.Tests: 6253 passed. Full-solution hermetic run: 15,410 passed across all projects, 0 failed. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
bc48e3216a
commit
4ade9b0427
7 changed files with 834 additions and 38 deletions
File diff suppressed because one or more lines are too long
Loading…
Add table
Add a link
Reference in a new issue