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>
16 KiB
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
- 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, INCLUDINGArmorLevels = 0x4000(nine u32s).Parsed.ArmorLevelsis a dead field — no production reader. No wire/parse work is needed. - Everything missing already arrives from ACE on a successful player assess (see §3/§4). The whole campaign is client-side composition.
- The core defect is an element MIS-MAPPING in
AppraisalUiController.ApplyCreature(character: true)(src/AcDream.App/UI/Layout/AppraisalUiController.cs:673-708): retail'sCharExamineUIctor@0x004AD3C0binds0x10000150= 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). - Zero tests exercise
AppraisalView.Character— how the mis-mapping survived the original Slice 3 gate (that gate checked page selection only). - 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: 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/UiTemplateListSlotrows from DAT template0x10000166;CreatureAppraisalLayeredListtwo-list chrome/foreground split over oneUiScrollable; animated clone inUiViewport 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 byExtraRatingsFollowRetailGroupingFormattingAndSeparators. BuildExtra takes only aPropertyBundle— it cannot seeParsed.ArmorLevels(signature change needed; armor rows precede rating rows). - Attribute rows: nine ordered rows +
???failure semantics + enchant bit styles pinned inCreatureAppraisalRowsTests. - 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
BuildTitlebehavior — 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 (
* = Unenchantablelegend 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 viaPlayerKillerStatusBitfield, 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 readRuntimeAllegianceStatefor another player's rank.