# 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: 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 (+` ~ ` band): bit1→"Celestial Hand"+Int 287, bit2→"Eldrytch Web"+Int 288, bit4→"Radiant Blood"+Int 289, none→`???`. Bands: 1–100 Initiate, 101–300 Adept, 301–600 Knight, 601–1000 Lord, 1001–1500 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 (value−9999) 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 4–6 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 " ") + 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.