acdream/docs/research/2026-08-25-campaign-as-ground-truth.md
Erik 0332466fdc 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>
2026-08-25 08:19:38 +02:00

239 lines
16 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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.