docs: Campaign AS plan + AS1 ground-truth synthesis (assess window, player targets)

Owner report 2026-08-25: the examination window on an assessed PLAYER is
missing retail's identity block (gender/heritage, title, PK), the three
per-bodypart armor-level rows, allegiance/faction lines, and the
target-configurable extras. Three-lens research (our pipeline, ACE wire,
named retail decomp) established: the 0x00C9 parse is already complete
(ArmorLevels 0x4000 parsed and dead), everything arrives from ACE, and
the core defect is an element mis-mapping in the Character subview vs
CharExamineUI @0x004AD3C0 (heritage slot fed a raw string prop, title
slot fed AllegianceName, PK slot fed MonarchsName, allegiance slot fed
an invented "Assessment incomplete" literal). Zero Character-subview
tests existed, which is how it survived the Slice 3 gate.

Slices AS2-AS5 (serial, coupled files) + AS6 gate script; ground truth
doc carries the binding line-composition tables, gap ledger G1-G10, and
rulings R1-R8.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Erik 2026-08-25 08:19:38 +02:00
parent 974fe88ade
commit 0332466fdc
2 changed files with 373 additions and 0 deletions

View file

@ -0,0 +1,134 @@
# Campaign AS — assess/examination window retail parity (player targets)
**Status: EXECUTING — AS1 research synthesis complete 2026-08-25; slices final.**
Owner report (2026-08-25, side-by-side screenshots, acdream vs retail, both
assessing the player "Dww"): acdream's examination window on a PLAYER target
is missing retail's identity block and body/config sections.
Missing vs retail:
1. **Identity block** under the title bar: gender + heritage ("Female
Aluvian"), current display title ("War Mage"), PK status
("Non-Player Killer") — none of it rendered by acdream.
2. **Per-bodypart armor levels**: retail shows three grouped rows —
"Head/Chest/Groin AL: x/y/z", "Bicep/Wrist/Hand AL: x/y/z",
"Thigh/Shin/Foot AL: x/y/z". Absent in acdream.
3. **Target-configurable extras**: retail only shows what the assessed
player configured to show (date of birth, age, number of deaths, chess
rank, fishing skill, damage/crit ratings, …). acdream shows a
"Dmg/CritDmg Rating:" line but not the rest of the family.
4. **Allegiance/faction lines**: allegiance name/patron/faction shown when
the target has them. Absent in acdream.
Explicit owner rulings:
- **The animated 3D paperdoll is an INTENTIONAL acdream deviation** (retail
shows a static tinted preview; its colors are buggy in retail). Keep it.
Register row required (added in the first slice that touches the window).
- Retail comparison is the oracle for text composition; all strings come
from DAT StringTables per the decomp — **never hardcoded English
literals**.
- All UI work goes through the standard GUI classes (UiLabel / UiPanel /
UiScrollablePanel / retail chrome) — no bespoke widgets, no quick fixes.
## Execution model (set by owner)
- **Fable** plans and coordinates (this doc + slice contracts + synthesis).
- **Sonnet** implements each slice against a pinned contract.
- **Opus** runs the dual-lens review per slice — retail-faithfulness lens +
architecture lens — followed by a fix round; slice is REVIEW-CLOSED only
after the re-review accepts the fixes.
- Commit to the worktree branch (`claude/windmill-seam-and-solid-polys`) as
slices land, full hermetic suite green each time
(`Lane!=InstalledDat&Lane!=PreparedPackage&Lane!=Live&Lane!=Manual&Lane!=Timing&Lane!=Windows&Lane!=Linux&Lane!=SystemFont&Purpose!=Diagnostic&Status!=KnownFailure`).
- **Do NOT push to gitea until the owner says so.**
- Campaign stops when all slices are REVIEW-CLOSED and the connected-gate
script is written and waiting on the owner's drive.
## Ground truth sources
- `docs/research/named-retail/acclient_2013_pseudo_c.txt` — the retail
examination window class, line composition, StringTable keys, 0x00C9
client-side parse.
- `references/ACE/` — what our live server actually sends per flag, and the
target-option gating rules.
- `references/Chorizite.ACProtocol/` — field-order cross-check.
- Campaign CT sealed verdicts (`claude-memory/project_character_panel_campaign.md`)
— the heritage/title composition and PK bitfield rules already ported for
the character panel; REUSE, do not re-derive.
- Research synthesis doc (AS1 output):
`docs/research/2026-08-25-campaign-as-ground-truth.md`.
## Slices (FINAL — re-cut at AS1: the 0x00C9 parse is already complete and
the appraisal profile is session-scoped UI presentation state flowing
through the established router seam, so the skeleton's wire and
runtime-owner slices are unnecessary; all work is App-side composition)
**Oracle for every slice:**
`docs/research/2026-08-25-campaign-as-ground-truth.md` (AS1 synthesis) —
its §2 line-composition tables, §3 wire truth, §4 gap ledger G1G10, and
§5 rulings R1R8 are BINDING on implementers and reviewers. Deviating from
a ruling requires a plan-doc amendment, not an implementer judgment call.
- **AS1 — ground truth synthesis (DONE, Fable).** Three research lenses
merged; gap ledger G1G10; rulings R1R8.
- **AS2 — header identity block (Sonnet).** Fix the element mis-mapping in
`AppraisalUiController.ApplyCreature(character: true)` per ground truth
§2a: `0x10000150` ← composed gender+heritage (reuse
`CharacterIdentityText`, including retail's heritage-id overrides and the
creature fallback when heritage==0); `0x10000151` ← current display title
via `CharacterTitleResolver` (Int 261, fallback String 5 Template);
`0x10000152` ← PK line from the assessed `ClientObject`'s PWD bits
(`PlayerKillerStatusBitfield`, ruling R7); `0x1000053A` ← String 47
AllegianceName gated on Int 30 ≥ 1, DELETING the invented
"Assessment incomplete" literal. First-ever `AppraisalView.Character`
controller tests (G10). Register: file the animated-paperdoll AD row
(owner-ruled intentional deviation 2026-08-25) in this commit.
- **AS3 — armor-level rows + extras-list plumbing (Sonnet).** Plumb
`Parsed.ArmorLevels` into the extras composer (signature change from
bare `PropertyBundle`); emit the spacer + three grouped AL rows before
the rating rows per ground truth §2b rows 37, including the `*%d`
≥9999 unenchantable rendering, retail's per-row rating gates
(307|313|314 · 308|315|316 · 350|351), spacer discipline per ruling R4,
and the `* = Unenchantable` legend per ruling R3 (unconditional,
gate-verified). Ordering pinned by tests.
- **AS4 — society/allegiance/fellowship + configurable extras (Sonnet).**
Ground truth §2b rows 12 and 814: the Society row with rank bands and
the local-vs-target faction color rule; the Monarch/Patron/Followers
cascade; Fellowship; Arrived in Dereth; Time in Dereth (locate or port
`ClientUISystem::DeltaTimeToString` — grep for an existing port first);
Chess Rank; Fishing Skill; Deaths ("Has never died" at ≤0); Titles
Earned. Register: narrow AP-110 ("exhaustive character detail regions"
clause retires) in this commit.
- **AS5 — allegiance rank-title table (Sonnet).** Port the 17-function
heritage×gender `AllegianceSystem::GetTitle @0x005B8DD0` table (census
per AP-109's corrected 2026-08-25 text: 11 heritages → 17 functions,
~170 strings; Gearknight/Tumerok male-only reused, Lugian female-only
reused, Penumbraen aliases Shadowbound, Olthoi excluded by the unsigned
range check) + `AllegianceData::GetFullName @0x005B6950`; wire the
examination title bar (rank from `props.GetInt(0x1E)`, ruling R8) AND
the character panel's name line (closing AP-109's residual). Register:
retire AP-109 in this commit.
- **AS6 — connected gate script (Fable).** User-driven script
`docs/research/2026-08-25-campaign-as-test-script.md`; two-client where
needed (allegiance/fellowship/PK lines, deception-failure rendering);
includes the R3 legend retail-side-by-side check and the owner's
configurable-extras toggle matrix.
Slices AS2→AS3→AS4 are SERIAL (all touch `AppraisalUiController` /
`CreatureAppraisalRows` — coupled-file rule); AS5 may run after AS4's
review closes. Each slice: Sonnet implements → full hermetic suite green →
commit → Opus dual-lens review (retail-faithfulness + architecture) → fix
round → narrow re-review → REVIEW-CLOSED.
## Ledger
| Slice | State | Land / fix commits | Notes |
|---|---|---|---|
| AS1 | **DONE 2026-08-25** | (docs commit) | 3-agent research; ground-truth doc committed |
| AS2 | ready to dispatch | — | |
| AS3 | pending AS2 review-close | — | |
| AS4 | pending AS3 review-close | — | |
| AS5 | pending AS4 review-close | — | |
| AS6 | pending AS5 review-close | — | |

View file

@ -0,0 +1,239 @@
# Campaign AS ground truth — assess/examination window, PLAYER targets
**AS1 synthesis (2026-08-25).** Three-lens research: our pipeline map, ACE
wire truth (`references/ACE`, cross-checked byte-identical against
`references/Chorizite.ACProtocol`), and the named 2013 retail decomp
(`docs/research/named-retail/acclient_2013_pseudo_c.txt`). This document is
the ORACLE for every Campaign AS slice. Addresses are acclient 2013
v11.4186; property ids verified against ACE enums.
## 0. The headline findings
1. **The 0x00C9 parse is already complete.** `AppraiseInfoParser`
(`src/AcDream.Core.Net/Messages/AppraiseInfoParser.cs`) parses every
flag retail parses, in retail's exact section order, INCLUDING
`ArmorLevels = 0x4000` (nine u32s). `Parsed.ArmorLevels` is a dead
field — no production reader. **No wire/parse work is needed.**
2. **Everything missing already arrives from ACE** on a successful player
assess (see §3/§4). The whole campaign is client-side composition.
3. **The core defect is an element MIS-MAPPING** in
`AppraisalUiController.ApplyCreature(character: true)`
(`src/AcDream.App/UI/Layout/AppraisalUiController.cs:673-708`): retail's
`CharExamineUI` ctor `@0x004AD3C0` binds `0x10000150` = Heritage,
`0x10000151` = Profession(title), `0x10000152` = PlayerKiller,
`0x1000053A` = AllegianceName — we feed them raw-HeritageGroup-string /
AllegianceName / MonarchsName / the invented literal
`"Assessment incomplete"` (zero retail provenance; the only occurrence
in the repo).
4. **Zero tests exercise `AppraisalView.Character`** — how the mis-mapping
survived the original Slice 3 gate (that gate checked page selection
only).
5. Retail composes these lines from `u"..."` code literals (not
StringTables), same as the item report — matching retail means literals
here, with DAT/EnumMapper lookups exactly where retail does them
(gender/heritage/title chains, attribute names).
## 1. Retail class map
`gmExaminationUI` (element-type 0x1000001C, `Register @0x004AB780`;
pinnable `gmFloatyExaminationUI` = 0x1000004C). LayoutDesc `0x2100006B`
(DAT-side; already our mount). Sub-UIs implement `ExamineSubUI`
(`acclient.h:54747`): `Init` / `SetAppraiseInfo(profile, isNewObject)` /
`Show`.
| Class | Methods that matter |
|---|---|
| `gmExaminationUI` | ctor `0x004AB2B0`; `PostInit 0x004AD6A0`; `RecvNotice_ExamineObject 0x004AB7B0`; dispatcher `SetAppraiseInfo 0x004ADAE0`; `SetTitleText 0x004AD930`; `UseTime 0x004AB530` (0.75 s combat refresh) |
| `BasicCreatureExamineUI` | ctor `0x004ACD30`; `SetAppraiseInfo 0x004B3F70`; `SetLevelValueText 0x004B3E70`; `AddLineToMiscInfo 0x004ABBB0` |
| `CreatureExamineUI` (monsters) | `SetAppraiseInfo 0x004B3FF0` |
| **`CharExamineUI` (players)** | ctor `0x004AD3C0`; **`SetAppraiseInfo 0x004B45F0`** — the full player line composer |
Sub-UI dispatch (`0x004ADAE0`): creature profile AND (String 5 Template OR
Int 261 CharacterTitleId) → CharExamineUI. Our `SelectView`
(`AppraisalUiController.cs:920-928`) already matches.
Widget ids: title `0x1000012D`; level value `0x1000014C`; attribute list
`0x10000149`; extra list `0x10000335`; row template label `0x1000012A` /
value `0x1000012B`; header texts `0x10000150/0x10000151/0x10000152/0x1000053A`;
viewport `0x10000148`.
## 2. Player line composition (CharExamineUI::SetAppraiseInfo @0x004B45F0)
"colorIdx" = 4th arg of `UIElement_Text::SetTextWithFont @0x0046A500`
the authored alternate-font-color index (attr 0x1B/0x1D list): 0 default,
**1 = buffed/green, 2 = debuffed/red, 3 = unknown/failed** (semantics
pinned by ACE `CreatureProfile.cs:44-45`). Exact authored RGBA lives in
LayoutDesc 0x2100006B (DAT), not the binary.
### 2a. Fixed header elements
| Element | Source | Composition |
|---|---|---|
| Title bar `0x1000012D` | `SetTitleText @0x004AD930`: String 52 override else appropriate object name; stack-count prefix | then the char path OVERWRITES with `AllegianceData::GetFullName @0x005B6950` = `AllegianceSystem::GetTitle(rank=Int 30, heritage=Int 188, gender=Int 113) + " " + name` ("Baroness Aluvia"); plain name when no rank title |
| Heritage `0x10000150` | Int 113 Gender, Int 188 HeritageGroup (Int 2 CreatureType only if heritage==0) | `AppraisalSystem::InqGenderHeritageDisplay @0x005B5AE0``"<Gender> <Heritage>"`. Gender: EnumMapper `0x10000001`; heritage: EnumMapper `0x10000002` with hardcoded overrides `2→"Gharu'ndim"`, `5→"Umbraen"`, `13→"Olthoi"`; creature fallback EnumMapper `0x10000005` (underscore→space) |
| Title `0x10000151` | Int 261 CharacterTitleId, fallback String 5 Template | `CharacterTitleTable::GetCharacterTitleFromID @0x005C6ED0`: EnumMapper `0x10000006``compute_str_hash` → StringTable behind table-enum `0x10000007`. Current display title only. Same pipeline as `CharacterTitleResolver` (CT campaign). |
| PK `0x10000152` | **NOT from the appraisal payload** — local `cur_weenobj` PWD bits: `IsPK() @0x0058C8B0``u"Player Killer"`, `IsPKLite() @0x0058C8A0``u"Player Killer Lite"`, else `u"Non-Player Killer"`. Skipped (left cleared) if the weenie is gone. Ours: `ClientObject.PublicWeenieBitfield` + `PlayerKillerStatusBitfield` (bits 0x20 / 0x02000000) — the exact port already exists (`ClientObject.cs:439-451`, per AP-109's CT4 correction). |
| Allegiance `0x1000053A` | String 47 AllegianceName | plain set, element cleared first; only inside the `Int 30 AllegianceRank >= 1` gate |
| Level `0x1000014C` | Int 25 | `SetLevelValueText @0x004B3E70`: >0 → comma-grouped; ≤0/absent → `"???"` |
### 2b. Extra-info list rows (list `0x10000335`), EXACT ORDER
Every row = `AddLineToMiscInfo(label, value, colorIdx) @0x004ABBB0`.
Literals: `"%d"` @0x794344/0x7A0184; `"*%d"` @0x7B110C; `u"???"` @0x7B0F34;
`L""` spacer @0x794320.
| # | Gate | Label | Value | Color |
|---|---|---|---|---|
| 1 | Int 281 Faction1Bits ≠ 0 | `Society:` | society name (+` ~ <rank>` band): bit1→"Celestial Hand"+Int 287, bit2→"Eldrytch Web"+Int 288, bit4→"Radiant Blood"+Int 289, none→`???`. Bands: 1100 Initiate, 101300 Adept, 301600 Knight, 6011000 Lord, 10011500 Master; outside bands → name alone | vs LOCAL player's Faction1Bits (`InqInt 0x119` on own qualities): same → 1 (green), different faction → 2 (red), local none → 0 |
| 2 | Int 30 ≥ 1 | allegiance rows | Str 21 MonarchsTitle absent → `Alleg. Monarch:` + `%d Follower`/`%d Followers` (Int 35, clamp ≥0). Str 21 present, Str 35 PatronsTitle absent → `Monarch:`+Str21. Both present, equal → one row `Monarch/Patron:`. Different → two rows `Monarch:` / `Patron:` | 0 |
| 3 | any of 9 AL > 0 | spacer, then `Head/Chest/Groin`, `Bicep/Wrist/Hand`, `Thigh/Shin/Foot` | per part `"%d"`, or `"*%d"` with (value9999) when ≥9999 (unenchantable sentinel); value cell = `"AL: %s/%s/%s"`. Nine dwords in order head, chest, groin(=Abdomen), bicep(=UpperArm), wrist(=LowerArm), hand, thigh(=UpperLeg), shin(=LowerLeg), foot | 0 |
| 4 | Int 307\|313\|314 > 0 | spacer, then `Dmg/CritDmg` | `Rating: %d/%d` ← (307 DamageRating, 314 CritDamageRating); 313 gates only | 0 |
| 5 | Int 308\|315\|316 > 0 | [spacer if row 4 absent] `Dmg/CritDmg` | `Resist: %d/%d` ← (308, 316); 315 gates only | 0 |
| 6 | Int 350\|351 > 0 | [spacer if none yet] `DoT/Life:` | `Resist: %d/%d` ← (350, 351) | 0 |
| 7 | any of rows 46 shown | trailing spacer | | |
| 8 | Str 10 Fellowship | `Fellowship:` | verbatim | 0 |
| 9 | Str 43 DateOfBirth | `Arrived in Dereth:` | verbatim (server-formatted) | 0 |
| 10 | Int 125 Age | `Time in Dereth:` | `ClientUISystem::DeltaTimeToString(seconds)` | 0 |
| 11 | Int 181 ChessRank | `Chess Rank:` | `%d` | 0 |
| 12 | Int 192 FakeFishingSkill | `Fishing Skill:` | `%d` | 0 |
| 13 | Int 43 NumDeaths | `Deaths:` | ≤0 → `Has never died`, else `%d` | 0 |
| 14 | Int 262 NumCharacterTitles | `Titles Earned:` | `%d` | 0 |
| 15 | unconditional (per decomp — see R3) | `* = Unenchantable` in the LABEL slot, empty value | legend | 0 |
Then the tail calls `BasicCreatureExamineUI::SetAppraiseInfo @0x004B3F70`
(level + attribute token update).
Note: retail's ratings format strings are `"%Rating: %d/%d"` /
`"%Resist: %d/%d"` — the leading `%R` is invalid printf that msvcrt renders
as literal `R…`, so the display text is `Rating: x/y` / `Resist: x/y`. Our
existing `CreatureAppraisalRows.BuildExtra` output matches the display.
### 2c. Attribute/vital rows (list `0x10000149`)
Created once in ctor order **Str, End, Coord, Quick, Focus, Self** then
**Health (percent shown), Stamina, Mana**; labels via
`SkillSystem::InqAttributeName @0x005C8D90` (StringTable-backed).
`AttributeInfoRegion::Update @0x004F1D90`: `"%d"`, 0 → `"???"`; color:
success==0 → 3, else enchant HI bit → 1 (green) / LO-without-HI → 2 (red)
(`InqAttributeEnchantmentMod @0x005B5EC0`).
`Attribute2ndInfoRegion::Update @0x004F1E80`: `%d/%d`, Health
`%d/%d (%d %%)`; **success==0: Health shows `%d %%` ONLY; Stamina/Mana show
`???`**; same color rule on the MAX-vital bits.
## 3. ACE wire truth (what the local server sends for a player)
Serialization: flags dword, Success dword, then sections in retail's parse
order (Int 0x1 → Int64 0x2000 → Bool 0x2 → Float 0x4 → String 0x8 → DID
0x1000 → SpellBook 0x10 → ArmorProfile 0x80 → CreatureProfile 0x100 →
WeaponProfile 0x20 → HookProfile 0x40 → ArmorEnch 0x200 → WeaponEnch 0x800
→ ResistEnch 0x400 → ArmorLevels 0x4000). Byte-identical vs Chorizite.
Player targets set: Int (always), String (usually), DID (appearance dids),
CreatureProfile (always, even failed), ArmorLevels (Success only).
**ArmorLevels** (`ACE .../Structure/ArmorLevel.cs:85-96`): nine u32
Head, Chest, Abdomen, UpperArm, LowerArm, Hand, UpperLeg, LowerLeg, Foot.
Sent when `Success && (Player || !Attackable)`. Values are **BUFFED**
(base + enchant mod per covering Clothing layer, clamped ≥0); if EVERY
covering layer is unenchantable the part gets **+9999** (the `*` sentinel).
Chorizite's "BaseArmor*" naming is misleading — trust ACE.
**Identity ints:** Gender **113**, HeritageGroup **188**,
PlayerKillerStatus **134** (ACE enum NPK 0x02 / PK 0x04 / PKLite 0x40 — NOT
the PWD bit layout; retail reads the local weenie's PWD bits instead, and so
do we), CharacterTitleId **261**, NumCharacterTitles **262**, Level **25**.
The display strings never ride the wire — client-composed.
**Option-gated extras** (removed from the tables when the TARGET's option
is off; `AppraiseInfo.cs:352-366`): DateOfBirth str **43**
(AllowOthersToSeeYourDateOfBirth), Age int **125**, ChessRank int **181**,
FakeFishingSkill int **192**, NumDeaths int **43**, NumCharacterTitles int
**262**. Client renders what's present — no client-side option logic.
**Ratings** (Success only, nonzero only, straight into IntStats):
Damage **307** (+5 heritage-weapon bonus applied server-side),
DamageResist **308**, Crit **313**, CritDamage **314**, CritResist **315**,
CritDamageResist **316**, HealingBoost **323**, NetherResist **331**,
DotResist **350**, LifeResist **351**, GearMaxHealth **379**, PKDamage
**381**, PKDamageResist **382**. Retail renders only
307/314 · 308/316 · 350/351 (313/315 gate; 323 read but never rendered).
**Allegiance/faction:** AllegianceName str **47**; monarch targets get
AllegianceFollowers int **35**; non-monarch get MonarchsTitle str **21**
(server-composed "<rank title> <name>") + PatronsTitle str **35**;
AllegianceRank int **30**; Faction1Bits int **281** (masked to 0x7);
Society ranks ints **287/288/289**; Fellowship str **10**.
**Failure semantics:** failed assess (target's Deception beats examiner's
AssessPerson, target opted into AttemptToDeceiveOtherPlayers) → Success=0
but ACE STILL sends full int/string/DID tables; only CreatureProfile
attributes (ShowAttributes flag 0x8), ratings, and ArmorLevels are gated.
Target sees "X tried and failed to assess you!"; retries inside 5 s
auto-fail. Guid-not-found → `Flags=0, Success=0` only.
## 4. Current acdream state (what exists, what's wrong)
- Window: `AppraisalUiController` (LayoutDesc 0x2100006B, root 0x100005F2),
mounted as independent top-level floaty; subviews Item/Creature/
Character/Spell; `UiText` + `UiItemList`/`UiTemplateListSlot` rows from
DAT template `0x10000166`; `CreatureAppraisalLayeredList` two-list
chrome/foreground split over one `UiScrollable`; animated clone in
`UiViewport 0x10000148` (`CreatureAppraisalPresentation`).
- Route: `GameEventWiring.cs:1003``LiveSessionEventRouter.OnAppraisal`
`RetailUiRuntime.HandleAppraisal``Apply(Parsed)`.
- Stale-response rejection, 0.75 s visible-combat refresh, busy-cursor
balance: present and pinned.
- Ratings rows exist (`CreatureAppraisalRows.BuildExtra`,
`CreatureAppraisalRows.cs:97-146`) with the retail grouping pinned by
`ExtraRatingsFollowRetailGroupingFormattingAndSeparators`. BuildExtra
takes only a `PropertyBundle` — it cannot see `Parsed.ArmorLevels`
(signature change needed; armor rows precede rating rows).
- Attribute rows: nine ordered rows + `???` failure semantics + enchant
bit styles pinned in `CreatureAppraisalRowsTests`.
- Reusable CT ports: `CharacterIdentityText` (gender/heritage composition,
`CharacterIdentityText.cs:81-117`), `CharacterTitleResolver` (EnumMapper
0x22000041→hash→StringTable 0x2300000E — the SAME retail pipeline; the
decomp's 0x10000006/0x10000007 are element-scope enum ids resolved to
those DAT ids), `PlayerKillerStatusBitfield`.
### Gap ledger
| # | Gap | Fix slice |
|---|---|---|
| G1 | Heritage slot fed raw string prop instead of composed gender+heritage | AS2 |
| G2 | Title slot fed AllegianceName; title never displayed | AS2 |
| G3 | PK slot fed MonarchsName; PK never displayed | AS2 |
| G4 | Armor-level trio absent (parsed, never read) | AS3 |
| G5 | `0x1000053A` shows invented "Assessment incomplete" instead of AllegianceName | AS2 |
| G6 | Society/faction row absent | AS4 |
| G7 | Monarch/Patron/Followers rows absent | AS4 |
| G8 | Configurable extras absent (Fellowship/DOB/Age/Chess/Fishing/Deaths/Titles + legend) | AS3 (legend) + AS4 |
| G9 | Title-bar allegiance rank prefix absent (AP-109's 17-function table) | AS5 |
| G10 | Zero Character-subview test coverage | every slice adds |
## 5. AS1 rulings on the open questions
- **R1 (String 52 override):** keep current `BuildTitle` behavior — String
52 is absent for players; harmless either way.
- **R2 (Int 323 HealingBoost):** retail reads it and never renders it; we
keep discarding it. Do not add a row.
- **R3 (`* = Unenchantable` legend unconditional):** port as the decomp
shows — unconditional at the end of the char extras (it may be a BN
flattening; the CONNECTED GATE explicitly asks the owner to check retail
side-by-side; flip to conditional only on live evidence).
- **R4 (ratings spacer discipline):** the BN output lost the flag
assignments in CharExamineUI; use `CreatureExamineUI::SetAppraiseInfo
@0x004B3FF0`'s clean version of the SAME logic: one spacer before the
first ratings row, one trailing spacer if any rating row was emitted.
- **R5 (colorIdx RGBA):** authored in LayoutDesc 0x2100006B text attrs
(0x1B/0x1D). The row mechanism already supports authored color styles
(enchant bit styles pinned); reuse it, never hardcode RGBA.
- **R6 (literals):** the header/extras labels are code literals in retail —
match them as literals (consistent with the item report), with DAT
lookups only where retail does them (gender/heritage EnumMappers, title
chain, attribute names).
- **R7 (PK source):** from the assessed `ClientObject`'s PWD bits via
`PlayerKillerStatusBitfield`, NOT PropertyInt 134 (AP-109's CT4 lesson);
leave the element cleared when the object is no longer in the table.
- **R8 (rank source for GetFullName):** `props.GetInt(0x1E)` — the
appraisal bundle carries AllegianceRank; never read
`RuntimeAllegianceState` for another player's rank.