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

16 KiB
Raw Blame History

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 0x10000006compute_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() @0x0058C8B0u"Player Killer", IsPKLite() @0x0058C8A0u"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 " ") + 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:1003LiveSessionEventRouter.OnAppraisalRetailUiRuntime.HandleAppraisalApply(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.