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:
parent
974fe88ade
commit
0332466fdc
2 changed files with 373 additions and 0 deletions
239
docs/research/2026-08-25-campaign-as-ground-truth.md
Normal file
239
docs/research/2026-08-25-campaign-as-ground-truth.md
Normal 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: 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 "<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.
|
||||
Loading…
Add table
Add a link
Reference in a new issue