Commit graph

274 commits

Author SHA1 Message Date
Erik
aeac874dab Harden retail character selection recovery 2026-08-14 20:59:10 +02:00
Erik
6cfab727f1 Implement retail character management screen 2026-08-14 20:29:19 +02:00
Erik
2a81e813cb fix #396 crash: map WaitDialog class type 0x19 to UiDialogRoot; dialog-open failure lands on the contracted refusal path
The user's first click on a mapping button killed the client: the live
dialog catalog's wait root 0x31 carries retail class type 0x19 (WaitDialog),
which DatWidgetFactory left unmapped, so the root built as a plain
UiDatElement and RetailWaitDialogView's ctor threw out of UiButton.OnClick
into the render loop. The unit test missed it by standing the confirmation
fixture (type 0x13, mapped) in for the wait root — the structural-false-
negative class again. Pins: DatWidgetFactoryTests theory for both dialog
root types, plus an installed-DAT UiDialogRoot/0x3D/0x3E assertion in the
env-gated keyboard probe. OpenCaptureInstructions now converts a dialog
construction failure into its contracted 0-return (log + capture refused,
retail's own OpenMapWarnDialog failure shape) instead of crashing.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-14 13:48:29 +02:00
Erik
30fa6ee507 fix #394 #395 #396: OP8 re-gate round — caption font, retail key names, capture dialog
Three findings from the user's first Configure Keyboard look (OP8 gate,
2026-08-14), each root-caused against the named retail decomp:

- #394 row-caption font: the synthesized action-label UiText never set
  DatFont and fell to the debug bitmap font. The authored row template
  (0x21000009/0x1000002F, retail UIOption_ActionKeyMap) carries FontDid
  0x4000000A (18px serif) — Bind now takes resolveTemplateFont and applies
  the template's own authored font, resolved once per template pair.

- #395 key captions: raw enum spellings ("Shift+ShiftLeft") replaced by the
  port of CInputManager_WIN32::GetNameFromKey @0x00687F40 /
  GetNameFromKey_Internal @0x00687800 (RetailKeyNames): DAT string-table
  override by DIK-name hash (key enum 4 -> 0x2300000A, meta enum 5 ->
  0x2300000B, delimiter enum 3 -> 0x23000007 — GetDIDByEnum category 4,
  live-probed), else the OS keyboard layout's own key name ("SKIFT") via
  PlatformKeyNameProvider (Win32 GetKeyNameTextW — register row AD-96 for
  the DirectInput-vs-GetKeyNameText adaptation), else the DIK-suffix
  spelling. Bare modifier-key bindings show only the key name.

- #396 capture feedback: clicking a mapping button now opens retail's
  instruction dialog (InitiateBinding @0x004899D0 -> OpenMapWarnDialog
  @0x00488A00): a type-2 WAIT dialog on retail's MapWarn queue key
  0x10000001 with ID_ActionKeyMap_MapInstructions (0x23000004, ACTION
  variable interpolated), closed on key hit or ESC through the capture
  callback; capture is not armed if the dialog cannot open, matching
  retail. New RetailWaitDialogView (wait root 0x31 — same authored
  popup/message pair 0x3D/0x3E as the confirmation root, live-DAT probed)
  behind a shared IRetailDialogView presenter seam.

Probe evidence (env-gated, kept):
KeyboardConfigLiveMountProbeTests.ProbeKeyboardFontsAndKeyNameStrings.
Register: AD-96 filed. Gate script OP8 section updated (step 4 rewritten;
the "pressed/active state is enough" contract is retired).

Full Release solution suite green (13,424 passed / 4 skips).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-14 13:45:55 +02:00
Erik
be3e617a2b fix: trade gate round 4 - "The trade has been cancelled." + retail's
staged-item trading marker

- Cancel text: ClientTradeSystem::Handle_Trade__Recv_CloseTrade
  @0x0056DE30 shows "The trade has been cancelled." UNCONDITIONALLY
  (every close reason) as 0x1A ClientLocal - the yellow top-center
  SpewBox line. Wired at the router's onTradeClose beside ApplyClose;
  the string lives in ClientTextRefusals with its citation.
- Staged-item marker: retail's mechanism decoded end-to-end - the
  UIItem prototype (catalog 0x21000037) authors overlay child
  0x10000438 (sprite 0x06001DAE, the green frame + corner trade icon),
  bound @0x004E18FC and SetVisible(tradeState != 0) @0x004E2420;
  gmSecureTradeUI::AddItem @0x004CA801 sets
  ACCWeenieObject::SetTradeState(1) on YOUR staged items. Ported as:
  UiItemSlot.ShowTradeOverlay + TradeOverlaySprite (drawn over the
  icon), set on the trade window's self-grid cells; and
  RuntimeTradeState now borrows the canonical object table and
  maintains ClientObject.TradeState (1 at stage, 0 at remove/failure/
  reset/close/clear) - which also brings the ALREADY-PORTED placement
  policy's "You cannot move an item while it is being traded" refusal
  to life (its input field previously had no live producer).

Runtime 1,626, App 4,992/3 - green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-14 12:45:10 +02:00
Erik
ffc73f80bf fix: trade gate round 3 - the mount-time-captured dead command bus (ONE
root cause for every dead interaction) + retail's Total Items caption

The round-2 probes nailed it: the request seam fired for BOTH open
paths (use AND drag - "drag-release pick" -> "drag-on-player" ->
"request"), but no open-cmd, no wire-open, and no LiveCommandBus
drop-warning ever printed. MountSecureTrade captured
_bindings.Options.CommandBus() ONCE at mount time - the pre-session
surface whose Publish routes into a null route silently. CommandBus is
a Func for exactly this reason; the social mounts resolve it inside
each lambda. Every trade command - open (use + drag), accept (the
"unpressable" Trade button - the click FIRED, the publish died),
Clear All, close, and drop-on-grid staging - died on that one captured
bus. All six lambdas now resolve the Func per call.

Also: ID_SecureTrade_TotalItemsLabel probe-verified token-free
(fragments ["Total Items: ", ""], one ITEMS variable 0x004E8A23) and
composed via ResolveTemplate - the count texts read retail's exact
"Total Items: N". AD-95 RETIRED same-day.

The pre-feature stub-toast test row (drag-on-player option-on expecting
"Secure trade is not open.") now pins the SecureTradeRequested seam
instead. App suite 4,991/3 skips.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-14 12:34:31 +02:00
Erik
067cbea8a5 feat: secure trade with other players - wire, RuntimeTradeState, the
authored gmSecureTradeUI window, and both retail open paths

Three-lane research first (docs/research/2026-08-14-trade-lane{A,B,C}):
retail gmSecureTradeUI decode, the byte-exact ACE/decomp/holtburger
three-way wire agreement, and the acdream seam map (which found both
open paths ALREADY classified by the ported policy - OpenSecureTrade on
Use-a-player, StartSecureTrade on drag-item-onto-player with the
DragItemOnPlayerOpensSecureTrade option - dead-ending at a stub toast).

- Core.Net: TradeRequests builders (0x1F6-0x204, retail's CM_Trade
  senders byte-checked against ACE's readers; the ACE-discarded
  AcceptTrade echo carries zero-count item lists - AD-94), corrected +
  completed inbound parsers (0x1FD-0x208; the old AddToTrade parser
  missed the SIDE dword, TradeFailure missed the reason), delegate-hole
  registrars, six WorldSession sends. 10 golden-byte tests.
- Runtime: RuntimeTradeState, the third sibling J-owner (fellowship/
  allegiance shape): session-scoped, clears at generation reset (new
  stage Trade=14), staged teardown stage 11 (Identity/EntityObjects
  shift 12/13, TeardownStageCount 14 - the FA2-era per-stage-flag test
  caught the mapping exactly as designed), combined ownership ledger,
  event routing with ACE's wrong-initiator RegisterTrade landmine
  honored (partner = whichever guid is not mine). 7 conformance tests.
- App: SecureTradeUiController binds the dedicated authored LayoutDesc
  0x2100000D (root 0x1000007A - gmSecureTradeUI::PostInit's exact ids):
  partner name/status/count/grid, the authored 'Trade' accept toggle
  (accept <-> decline withdraw), 'Clear All' (ACE clears BOTH sides -
  surfaced honestly), the X close, drop-on-your-grid staging, per-mode
  accept cues (partner icon's authored Highlight state + Trade button
  Selected latch). Mounted via the vendor recipe (nine-slice chrome,
  hidden until RegisterTrade). ItemInteractionController's two policy
  arms now raise SecureTradeRequested instead of the stub toast; the
  drag path queues the dragged item until the window registers
  (ClientTradeSystem::AttemptToTradeItem @0x0056DF80's shape).

Register: AD-94 (accept-echo zero-count lists), AD-95 (numeric-only
count texts pending template verification).

Suites: App 4,990/3, Core.Net 905, Runtime 1,626 - all green. The
panel itself is user-gate acceptance (two-client connected trade), the
#372-class lesson: fixture-green alone is not acceptance for a mount.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-14 11:49:13 +02:00
Erik
e449c75e9f fix: friends list live Online/Offline status - authored row state
machine + UiText per-state string swap (user retail gate)

The state side already worked (FriendsState replaces the full entry and
bumps Revision on 0x0021 OnlineStatus updates; the parser reads full
FriendData for every update type). The UI side had two gaps, exposed by
the installed-DAT row-template probe (template layout 0x2100005D root
0x10000519):

- The row authors TWO cells: the LEFT name text 0x1000051A whose
  Online (0x10000054) / Offline (0x10000055) PassToChildren states
  cascade into the RIGHT status grandchild 0x1000051F, which authors
  per-state 'Online'/'Offline' strings AND per-state colors (retail's
  green Online). The controller's FindDeepest binding wrote the NAME
  into the STATUS cell (the deepest text IS the status grandchild) and
  never flipped the state machine - so the status column never showed
  or updated anything.
- UiText had no per-state authored-string swap: ApplyDatState switched
  sprite + color per state but never the 0x17 string. Ported now
  (second consumer of the mechanism after the powerbar caption):
  DatWidgetFactory pre-resolves each state's authored string;
  TrySetRetailState swaps the line, colored by the SAME state's
  authored 0x1B.

SocialFriendsPageController now binds the name to its own cell and
flips the authored Online/Offline state per friend on every
Revision-driven rebuild - the cascade renders the status cell exactly
as retail's gmFriendsUI does, green Online included.

App suite 4,989/3 skips (new per-state swap conformance test).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-14 10:57:57 +02:00
Erik
88cdfdc3c7 fix: jump-bar 'Height' right-aligned - per-STATE justification (user
gate; the DAT authors it)

The raw-property probe settles it exactly as the user reported: the
powerbar caption's mode STATES author their OWN justification (0x14
Enum=0x3 = Right on JumpMode/MeleeMode/MissileMode) while the element
default stays centered. ElementInfo.HJustify only ever read the
effective DEFAULT state, so the earlier "authored Center" conclusion
measured the wrong state.

The meter's absorbed state-label entry now carries the state's own
authored alignment (state 0x14 wins, element-level HJustify as the
fallback, ElementReader's same enum mapping), and the caption draw
aligns accordingly - 'Height' sits at the bar's right edge, retail's
placement. Live vitals labels keep their centered draw.

App suite 4,987/3 skips.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-14 08:22:03 +02:00
Erik
adf8335675 fix: powerbar mode captions - jump 'Height' + combat 'Power'/'Accuracy'
Retail authors ONE caption element per bar with per-mode state strings,
switched by a PassToChildren state cascade (gmPowerbarUI::
RecvNotice_BeginPowerbar @0x004DA730 sets 0x10000042 Jump / 0x10000043
Melee / 0x10000044 Missile / 0x10000045 DDD; installed-DAT probe
confirmed every string + PassToChildren flag).

- Jump bar (user gate): the floaty powerbar's caption child (0x10000035:
  JumpMode 'Height', authored HJustify=Center over the bar) was dropped
  by UiMeter's child absorption. The stateful-fill meter build now
  absorbs it into per-state labels; TrySetRetailState latches the
  caption and OnDraw shows it when no live Label provider is bound.
  JumpPowerbarController's existing JumpMode flip now surfaces 'Height'
  with zero controller changes. The mount gained the string resolver the
  Build call never passed.
- Combat bar (user gate): label 0x10000052 authors 'MeleeCombat' ->
  'Power' and 'MissileCombat' -> 'Accuracy'; the controller latched the
  MELEE string once at bind. CombatUiLabels now resolves both authored
  strings and OnCombatModeChanged sets the mode's string - switching
  live when swapping melee <-> missile weapons in combat. Also fixed
  the mode-state flip target: the states live on the BASIC PANEL
  (0x1000005C, PassToChildren), not the layout root (Hide/ShowDetail
  only) - the old _root flip was a silent no-op.

New env-gated ACDREAM_PROBE_POWERBAR layout probe (kept, house
pattern). App suite 4,987/3 skips.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-14 08:14:14 +02:00
Erik
4943484eb9 fix: social gate round 3 - authored multiline word wrap + geometric
move-cursor border band

- Empty-state text (round 3): the literal-\n split was correct but each
  authored LINE rendered as one clipped run. Retail word-wraps each
  authored line within the element extent (its GlyphList draw - the
  same wrap RetailConfirmationDialogView already uses). Multiline
  authored text now wraps through UiText.WrapWords against the widget's
  LIVE width/font/color (cached per width+font+color, re-read per call).
  Single-line authored labels keep their one-run shape - re-wrapping
  every label is a client-wide change no gate asked for.
- Move cursor (round 3): "the frame won the hit-test" is not a border
  test - windows whose interior is not fully covered by children (the
  inventory panel's empty regions) resolve those pixels to the frame
  too. The border is now a geometric 8 px band along the window's outer
  edge, AND the frame must win the hit-test so border-adjacent content
  keeps its own cursor. Resize-edge claim still takes precedence;
  whole-surface dragging unchanged.

App suite 4,984/3 skips (new BuildText_MultilineAuthored wrap test).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-14 07:50:59 +02:00
Erik
67fe754dd6 fix: social gate round 2, part 2 - confirmation-dialog sentences + the
refused-drop yellow notice

Item 4 (confirmation dialogs missing text + names): the missing retail
mechanism was StringTable template substitution - an entry is N+1 literal
fragments interleaved with N named variables, composed by
StringTable::GetString @0x004300D0 (no-metalanguage branch @0x004303B7).
ACE sends the bare player name for types 1/4; retail's OWN CLIENT wraps
it. Ported as DatStringResolver.ResolveTemplate (PLAYER hash 0x05506DA2,
the exact compute_str_hash space; Chorizite stores the variable hashes
directly):

- Server-driven type 4 -> ID_Fellowship_FellowshipRequest, type 1 ->
  ID_Allegiance_AcceptSwearConfirmation, injected into
  GameplayConfirmationController; null resolve falls back to the bare
  wire message, never invented English. The 2/3/5/6 " Continue?" family
  never consults the composer.
- Local Swear/Break/Kick: the bind-time fragment-0 latch (which showed
  the dangling "Do you wish to swear to ") is replaced by click-time
  ResolveTemplate with the target's name.

All five templates verified token-free in the installed DAT - this is
NOT a StringTableMetaLanguage port (AD-81's engine caveat stands).

Item 5 (refused drop shows nothing; retail shows yellow top-center
text): the prevRequest latch was ALREADY ported (InventoryTransactionState);
what was missing was the consumer. InventoryTransactionState now raises
RequestFailed(request, weenieError) when a 0x00A0 clears the latch;
ItemInteractionController composes ServerSaysAttemptFailed @0x0058EAE0's
"The <item> can't be <verb>" (verb table + suffix map ported verbatim in
Core's InventoryFailureMessages, NAME_PLURAL for merge/split) and routes
it as LogTextType 0x1A ClientLocal -> the SpewBox, retail's yellow
top-center line. The dispatcher's second leg (@0x0055B342) also runs:
outside the 7-code exclusion set, WeenieErrorMessages resolves per-code
text/destination; 0x426 AttunedItem has no row in either place beyond
the verb line - faithful single-line output.

Register: AD-85 narrowed to its numeric-field item, AD-81 amended (the
token-free interleave is now ported; meta-token engine + FormatName
remain), AD-93 filed (wire-guid-match vs retail's latched-guid
preference; no Move/Wield latch kinds).

Tests: +2 InventoryTransactionState failure-latch, +5 ResolveTemplate
(constructed StringTable fixtures), +1 composer injection, +1 end-to-end
refused-drop line. Core 4,697/1 skip, App 4,983/3 skips.

Research: docs/research/2026-08-13-confirm-and-weenie-error-display.md

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-13 21:10:42 +02:00
Erik
fc62cb6397 fix: social gate round 2, part 1 - border-only move cursor, literal-\n
empty state, retail amber row selection

- Move cursor (user-directed, ALL windows): HoverWindowMove now
  advertises only where the window frame element itself wins the
  hit-test - its border pixels; interior points resolve to content
  children. Matches retail's Dragbar-chrome-only move cursor.
  Whole-surface dragging still works, it just does not advertise.
- Empty-state text (round 2): the DAT stores the LITERAL two-character
  escape backslash-n (probe-verified - the dump printed the escape, not
  line breaks), so the round-1 newline split never matched. Escapes are
  normalized before splitting in DatWidgetFactory authored text.
- Selected fellow amber (user: "check retail"): probe-verified - the
  row name band 0x10000282 AUTHORS the retail selected-row art
  (DirectState 0x06001450 + Highlight 0x06001451, the amber). Selection
  flips the band's ActiveState to Highlight; no invented tint.

App suite 4,976/3 skips. Confirmation-text + refused-drop-notification
research (the round's items 4-5) lands as part 2.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-13 20:36:24 +02:00
Erik
72ceddce2e fix: social panel completion batch (user gate 2026-08-13, "fix all")
One user-ordered batch across the FA social panel + world selection.
Every root cause was probe-proven before the fix (new
ProbeSocialClickRouting in SocialPanelLiveMountProbeTests - production
window mount + real UiRoot hit-tests + a synthetic click):

1. STUCK CHECKBOXES (fellowship x4, allegiance x1, "always checked /
   can't change any options"): the authored checkboxes carry DAT
   ToggleBehavior, so UiButton SELF-FLIPS Selected at MouseUp - the old
   handlers read the flipped value and wrote the ORIGINAL back, snapping
   every click to where it started (the probe recorded (id, oldValue)).
   Fix: SuppressSelfToggle (the CH6a/b mirror discipline) + derive the
   next value from the STORE; the per-tick seeding mirrors it back.
2. UNCLICKABLE ROSTER ROWS ("only get the move window cursor"): the row
   name text is display-text ClickThrough=true, which the hit-test walk
   skips regardless of HandlesClick - the wired OnClick was unreachable.
   Fix: UiText.OnClick assignment now clears ClickThrough (central,
   documented); the stats text gains the same select handler so most of
   the row's width selects the fellow.
3. TRUNCATED EMPTY-STATE ("You do not belong... To create MISSING"):
   the authored string resolves COMPLETE (three sentences) but embedded
   '\n's rendered as one clipped line. DatWidgetFactory now splits
   authored strings into one Line per newline, with the provider still
   re-reading DefaultColor live (the state-color contract - caught by
   BuildText_AuthoredLineTracksStateFontColor).
4. FELLOW NAMES WHITE (user-directed): the AD-82 invented leader-gold +
   selection-blue tints are deleted; names always white (register row
   narrowed).
5. ALLEGIANCE HEADER LABELS: bare "0"/"0" -> "Followers: N" / "Rank: [N]"
   (user-specified format; the full retail StringInfo composition stays
   AD-85's gap), monarch block matching.
6. FRIENDS/SQUELCH LIVE (AD-79 mostly retired): Add friend (name box ->
   0x0018, retail clears the box - Request_AddFriend @0x0048D240),
   Remove (row-click selection -> 0x0017), Appear Offline (CharacterOption
   0x27 via the immediate 0x0005 auto-save, ACE pushes FriendStatusChanged
   to your friend-of list), Squelch Character/Account add-by-name
   (0x0058 guid0/type AllChannels + 0x0059) and Remove for the selected
   row. The wire beneath (builders, WorldSession sends, Runtime commands,
   parsers) existed end-to-end since J4.1/FA1 - this is panel wiring only
   (docs/research/2026-08-13-social-wire-completion.md, committed here).
   Send Tell stays inert (not in the order; AD-79's remainder).
7. WORLD SELF-SELECTION ("clicking my own char should select myself"):
   retail has NO self-exclusion (CPhysicsPart::Draw @0x0050D823 arms
   every physobj; RecvNotice_SmartBoxObjectFound @0x004E5BAE selects
   unconditionally) - the includeSelf gate was an unregistered
   divergence, now removed on both the left-click and right-click paths.

Element roles were probe-measured, never guessed (Add 0x10000514 /
Remove 0x10000515 / Send Tell 0x10000516 / Appear Offline 0x1000052C /
name field 0x1000051B; Squelch: field 0x10000540, Remove 0x10000547,
Squelch Character 0x1000054B, Squelch Account 0x1000054C).

Register: AD-79 mostly retired, AD-82 narrowed. Known remainder, filed
not hidden: the fellowship page's authored 600px content vs the 362px
viewport leaves Dismiss/Assign-Leader below the fold until the window is
resized taller (probe-measured; candidate follow-up).

Tests: Checkbox_Click fact rewritten to the mirror contract (both
directions), monarch-followers label updated, includeSelf expectation
updated, probe extended (click routing, synthetic click, action-widget
role dump). App suite 4,976/3 skips.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-13 19:30:51 +02:00
Erik
13d388e5a9 fix #391: curated modern-only resolution list from the monitor's modes
User-directed (2026-08-13): "we should only support modern resolutions.
Not any old format." New DisplayModeCatalog enumerates the window's
monitor (Silk IMonitor.GetAllVideoModes) once at GameWindow load and
curates via a pure, tested rule: modern widescreen families only
(16:9/16:10/21:9/32:9 within 2.5%), at least 1280 wide, must fit the
desktop (an impossible windowed pick is not offered - the measured
3840x2160-on-2560x1440 silent clamp class), desktop mode always
included, refresh-rate duplicates collapsed, ascending order.

The Config Resolution row consumes the catalog through two new optional
Bind parameters; its Defaults value becomes the desktop's own mode.
Fixture/headless callers keep the static preset ladder, which now drops
800x600 and is pinned by test to pass the same curation rule (the OP6 S4
"default must be re-selectable" invariant holds on both paths).

Deliberate retail deviation, register row IA-22: retail listed the
adapter's complete enumeration including 4:3 legacy modes and authored
800x600 as the Config default (gmConfigUI::InitOptions
SetDefaultValue(0x03200258); gmClient::Init @0x004047af). The catalog is
also the designated fullscreen mode-switch validation source for
#376/#388 - an offered mode is supported by construction.

Tests: DisplayModeCatalogTests (8 - filter/clamp/dedupe/sort/ultrawide/
desktop-inclusion/fallback-consistency); ConfigOptionsPageControllerTests
row-12 default updated. App suite 4,961/3 skips; UI.Abstractions 916.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-13 17:16:09 +02:00
Erik
a9b6435f55 fix #385: Options dropdowns — white centered text + size-to-content popup
User gate report (Campaign OP happy-testing round, 2026-08-13): every
Config-tab dropdown drew its text gold + left-aligned and its popup a
fixed 6 rows regardless of item count. All three were unmeasured styling
divergences — the authored data (new probe menuprobe3, live DAT) says:

- button label child 0x10000355: fontColor white, hJustify=Center
- row template 0x1000035A: fontColor white, hJustify=Center
- popup ListBox 0x10000358: edge-docked L=T=R=B=1, the authored condition
  arming retail UIElement_Menu::RecalculatePopupSize @0x0046caf0 —
  popup resizes to the ListBox's summed content height, uncapped
  (0x0046e5f4..0046e66c via ResizeScrollableArea's 0x32 broadcast)

UiMenu gains three opt-in properties (ButtonTextCentered,
ItemTextCentered, PopupSizeToContent) plus retail Open @0x0046cc42's
empty-list gate; chat + vendor keep the class defaults, so their shipped
behavior is untouched. ConfigOptionsPageController.ApplyMenuChrome wires
all four corrections for the 8 Config menus with the probe citation.

The same probe found vendor's authored popup ListBox is ALSO docked while
our vendor dropdown ships G5's fixed 6-row window — filed as #386 +
register row AD-88 (UNCLEAR: the G5 retail screenshot and the decomp
mechanism conflict) instead of silently reworking a user-gated surface.

The "resolution change resizes the window" observation from the same
report is #374's designed windowed-mode behavior (display-mode switching
is #376/#377) — no change.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-13 08:51:57 +02:00
Erik
eac28dc1f0 fix(ui): FA5 mechanism-review SF-1 — remove the invented offline-vassal name-grey
The FA5 mechanism review found the offline-vassal name-grey
(OfflineNameColor) is an invented visual: retail's UpdateVassalsData
@004924c3 writes the vassal name with no colour change, and the offline
cue is EXCLUSIVELY the authored 0x100004AA marker (already wired,
SetVisible per online state). Removed OfflineNameColor; the vassal name
always renders in the normal white. The Allegiance page now carries NO
invented tint (unlike Fellowship's registered leader/selection tints).
Pinned by Allegiance_OfflineCue_IsTheMarkerOnly_NameStaysWhite (marker
visible iff offline, name always white). AD-82's FA5 addendum corrected
(it had described the now-removed grey as 'covered by the marker'); AD-86
count corrected seven -> nine.

Full Release suite: 13,297 passed / 4 skipped / 0 failed.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-12 09:07:58 +02:00
Erik
7ed79eaf10 feat(ui): FA5 -- allegiance page fully live: CF-1 subscription, blocks, roster, swear/break/kick
Campaign FA slice FA5. The Allegiance page (0x10000291) goes from FA3's
empty-state shell to fully live, wired against FA1's parser and FA2's
RuntimeAllegianceState/IRuntimeAllegianceCommands (both already shipped
the full command surface, including SetUpdateSubscription).

CF-1 (the corrected data subscription): 0x001F AllegianceUpdateRequest,
not 0x027B, is the panel's data source (0x027B/0x027C are text-only chat
per FA2 MF-2). Wired at retail's three arming points -- Bind's PostInit
attempt (almost always a pre-world no-op), the post-world EnteredWorld
seam (RedeclareAfterWorldEntry, UNCONDITIONAL -- does not check the
current latch, matching retail's own PlayerDescReceived arm and avoiding
the exact MF-3-REOPEN bug class FA4 hit for 0x00A6), and the visible
branch (SetPageVisible, edge-triggered, folded into
SocialPanelController's existing window-shown+active-tab conjunction
alongside Fellowship's 0x00A6).

Monarch/patron/self blocks: per-relationship empty-state gate (fix-round
SF-7) replacing FA3's coarse HasProfile-only gate -- the monarch block
hides when there is no monarch OR the monarch is the viewer; the patron
block hides when there is no patron OR the patron is the monarch (in
which case the monarch block's 0x10000490 sub-block reveals and its
label swaps to PatronSlashMonarchLabel). Field sources decompiled fresh
from gmAllegianceUI::UpdatePlayerData/UpdateMonarchData/UpdatePatronData:
0x10000251 is the ALLEGIANCE's own name (not the viewer's), follower
counts are TotalVassals/TotalMembers-1 directly off the wire, and the
"experience passed up" text (0x10000492, doubled -- scoped FindDescendant
under each of its two parents) is the viewer's own CpTithed under the
monarch/patron blocks and each vassal's own CpTithed in their row.

Vassal roster: flat list built via UiTemplateListBox.FlushPreservingScroll
in the FA4 roster-diff pattern (guid-set diff, in-place update on an
unchanged set), rendering in the bindings' own already-reversed order.

Swear/break/kick: each opens a local confirmation dialog
(RetailDialogFactory via ShowConfirmation) before sending, mirroring
retail's MakeSwearConfirmationDialog family -- Swear targets the WORLD
selection (via the same ClientObjectTable name resolver
ToolbarRuntimeBindings.ResolveName already uses), Break targets the
current patron, Kick targets the panel-local selected vassal row (no
world-selection sync for Allegiance, unlike Fellowship). The
server-driven "accept incoming swear" (ConfirmationType 1) needed no new
code -- GameplayConfirmationController already handles every type
generically; a new test verifies it explicitly.

Runtime/composition plumbing: DeferredGameRuntimeStateCommands gains
Allegiance{Swear,Break,Kick,SetUpdateSubscription}; SocialRuntimeBindings
gains the Allegiance view/command projections; SocialPanelController.
Callbacks.AllegianceSnapshot widens to a full
SocialAllegiancePageController.Bindings record, mirroring FA4's
Fellowship widening.

Tests: SocialPanelControllerTests.cs gains 10 tests covering the SF-7
gate (4), roster population, swear/break/kick wiring (3), and the CF-1
subscription arming points (2); GameplayConfirmationControllerTests.cs
gains the type-1 verification test.
Also extends SocialPanelLiveMountProbeTests.cs (production-mount
assertions: scoped 0x10000492 resolution, the vassal row template, the
checkbox, confirmation-dialog string resolution, and a full production
Bind() pass) -- not yet run against live DATs in this worktree (no
Documents/Asheron's Call present here).

Release build green; full solution suite 13,296 passed / 4 skipped / 0
failed (13,300 total), up from FA4's 13,285/4/0 baseline.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-12 08:40:07 +02:00
Erik
04161defd8 fix(ui): FA4 re-review REOPEN — re-declare 0x00A6 from the post-world seam, not the pre-world reset
The FA4 fix round's MUST-FIX 3 placed the 0x00A6 reconnect re-arm at the
wrong lifecycle point (re-review 8bbceff5): ResetSessionTransientUi runs
via the SessionDialogs reset stage BEFORE _inWorld=true, so SetPanelOpen
(world-gated, Validate requireWorld:true) returned Inactive and published
nothing — yet _pageVisible was latched true anyway, so no later hook
re-declared and fellow vitals stayed frozen for the whole new session.
The unit test passed only because the fake recorded unconditionally.

Two-part fix, both retail-faithful mechanisms not suppressions:
- SocialFellowshipPageController.SetPageVisible advances the edge-trigger
  latch ONLY when the declaration is Accepted (published), so a dropped
  pre-world send leaves the latch clear and a later attempt retries.
- ResetSessionDeclaration (pre-world) now ONLY clears the latch; the new
  RedeclareAfterWorldEntry fires from the LiveSession EnteredWorld seam
  (wired via RestoreLayout, idempotent if a persisted layout already
  re-showed the page) so a still-open Fellowship page re-declares 0x00A6
  in world and vitals resume.

Regression pins that actually catch it (the prior test could not):
- SetPageVisible_DoesNotLatch_WhenDeclarationDropped_SoItRetriesInWorld
  (widget-level root, world-gated fake);
- Reconnect_ReDeclares0x00A6_AfterWorldEntry_NotDuringPreWorldReset +
  Reconnect_StaysSilent_WhenFellowshipPageIsNotActuallyOpen (panel-level,
  world-gated). RED-verified: reintroducing the pre-world declaration
  fails the reconnect test.

Full Release suite: 13,286 passed / 4 skipped / 0 failed.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-12 07:59:16 +02:00
Erik
55b17e15fd fix #FA4-mechanism-SF-6: assert (not just print) the live-DAT checkbox labels and Open/Close captions
SocialPanelLiveMountProbeTests wrote the four checkbox labels and the two
Open/Close captions to the console with no assertion, yet the FA4 ledger's
live-DAT paragraph cited them as verified -- the same finding FA3's own
mechanism SF-3 raised for a different table ("printed but never asserted
-- deserves a real assertion, not just a hope"). Now asserts each label is
non-null/non-empty and the two captions equal the exact retail strings
"Open"/"Close". Env-gated (ACDREAM_PROBE_LIVE_MOUNT=1, real installed
DATs) -- inert in this session's build/test run.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-12 07:33:50 +02:00
Erik
300d8189f6 fix #FA4-D6-D7,SF-8: revert three of FA4's four dimming un-dims (IgnoreFellowshipRequests, FellowshipAutoAcceptRequests, FellowshipShareLoot)
The corrected plan D6 (docs/plans/2026-08-11-fellowship-allegiance-campaign.md)
established that retail's client reads neither IgnoreFellowshipRequests nor
FellowshipAutoAcceptRequests on the fellowship-invite path -- both are pure
server-side filters with no client consumer, exactly like the two
allegiance bits they were always meant to parallel. Their claimed consumer
(RetailUiRuntime.TryAutoRespondToFellowshipInvite) is deleted in a sibling
commit this fix round. Both rows revert from Live to StoreOnly.

Mechanism review SF-8 additionally found FellowshipShareLoot's claimed
consumer -- "a second live checkbox surface on the fellowship page" -- is
not a consumer at all: nothing in acdream reads the stored value back
(FormatStatsText uses snapshot.ShareXp only; the 0x00A2 Create builder
carries shareXP alone), and the live-DAT dump confirms its checkbox is a
child of the NOT-in-fellowship frame -- invisible whenever you actually
have a fellowship to loot-share within. A second EDITOR of a value is not
a CONSUMER of it under AD-78's own "drives nothing observable client-side"
definition. FellowshipShareLoot reverts too.

Only FellowshipShareXP survives as genuinely live -- the Create-flow click
reads it directly as the sent shareXP wire bit. Net: 35 (pre-FA4) -> FA4
shipped 31 -> fix round reverts three -> 34 of 50 dimmed / 16 live, ONE
net un-dim from the pre-FA4 baseline, not four. Updated the class doc's
derivation table, the conformance test's ExpectedStoreOnlyIds set, and the
31/19 count assertions to 34/16.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-12 07:33:42 +02:00
Erik
df00030697 fix #FA4-mechanism-MUST-FIX-3,SF-4: re-arm 0x00A6 on reconnect; unsubscribe ActivePageChanged on Dispose
MUST-FIX 3 -- SocialFellowshipPageController.SetPageVisible is edge-triggered
on a bool that survives a generation reset unchanged while the panel stays
open, so a reconnect never re-sends 0x00A6 and fellow vitals freeze for the
rest of the new session. SocialPanelController.ResetSessionDeclaration
clears the fellowship page's latch (SocialFellowshipPageController.
ResetPageVisibleLatch, this commit's counterpart) and re-evaluates the
existing "window shown AND Fellowship active" conjunction, wired into
RetailUiRuntime.ResetSessionTransientUi -- a seam that already runs on
every generation reset. A still-open Fellowship page re-declares; a closed
or other-tab page correctly stays silent.

SF-4 -- SocialPanelController's constructor subscribed an anonymous lambda
to UiTabPanel.ActivePageChanged with no way to remove it; Dispose only set
a flag. A tab switch after Dispose still reached
UpdateFellowshipPageVisibility and issued a Runtime command, since Tick's
own _disposed guard doesn't cover this event path. Stored the handler as a
field and unsubscribe it in Dispose.

Also adds the panel-level D4 conjunction test mechanism SF-5 flagged as
missing (the only prior D4 test exercised the PAGE controller's own
edge-trigger directly, never SocialPanelController's "window shown AND
Fellowship active" logic or its ActivePageChanged subscription).

Per docs/research/2026-08-12-fa4-review-mechanism.md.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-12 07:33:32 +02:00
Erik
5499f0581f fix #FA4-mechanism-MUST-FIX-1,4: truncate (not round) the XP-share percentage; port the world->panel fellow-selection sync
MUST-FIX 1 -- D5's percentage conversion rounds where retail truncates.
gmFellowshipUI::UpdateFellowStats @0x0048ECC9 forms pct*100.0f on the x87
stack then calls _ftol2 (MSVC's round-to-truncate helper), never
MathF.Round. The stored floats for 6 and 8 fellows are 0.44999998807907104
and 0.3499999940395355 (byte-read from the PDB-paired binary), so retail's
own products truncate to 44/34, not 45/35 -- and (int)(pct*100f) alone does
not fix it, since 0.45f*100f already rounds UP to exactly 45.0f in single
precision. Fixed as (int)((double)pct * 100.0), forming the product the
same wider-than-single-precision way retail's x87 does. Pinned with new
[InlineData] cases for both sizes.

MUST-FIX 4 -- gmFellowshipUI::UpdateFellowSelection @0x0048F0F0 (the
world->panel arm of retail's two-directional selection coupling) was never
ported; only the panel->world arm (SelectFellow) shipped. Selecting a
fellow in the WORLD left Dismiss/Assign-Leader disabled and showed no row
highlight. SyncSelectionFromWorld/SetSelectedFellow reproduce the
observable contract (button-enable + a row tint) against this
controller's own guid-keyed row dictionary instead of porting retail's
generic ListBox SetAttribute_InstanceID/SetSelectedItem primitive (scoped
disposition recorded at register row AD-82).

Also in this pass over the controller:
- SF-1: cache the fellowship-name LinesProvider; only reassign on an
  actual name change (was allocating once per Tick, even while hidden).
- SF-2/SF-3: track true membership in _memberGuids, independent of which
  rows finished building. Fixes an unbounded DAT-locked rebuild retry
  when a row template permanently fails to build, and fixes Recruit's
  "already a fellow" check reading render rows instead of membership.
- N-0: the Open/Close caption now flips optimistically on click, matching
  retail's pre-toggle-before-server-echo (lane B feature 11).
- N-1/N-2/N-3: doc-only notes on the meter-child-text gap, the max>0
  guard, and Tick's two-read non-atomicity.
- ResetPageVisibleLatch: the fellowship-controller half of MUST-FIX 3
  (see the SocialPanelController commit for the panel-level half).

Per docs/research/2026-08-12-fa4-review-mechanism.md.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-12 07:33:18 +02:00
Erik
5bdd0528f1 feat(ui): FA4 -- fellowship page fully live
Roster: SocialFellowshipPageController now builds one row per fellow
from the authored template (0x21000030/0x10000281, live-DAT verified),
diffing the member GUID set on each revision-gated Tick -- an unchanged
set updates every row's bound widgets in place (no ListBox mutation, so
scroll position is untouched by construction); only a real join/leave/
disband triggers a rebuild, via UiTemplateListBox.FlushPreservingScroll
(FA3 carry-forward 1, both the widget-level fix and the controller-level
diff). Health/stamina/mana meters bind Fill+Label; the leader's name
tints gold (lane A's row template has no dedicated leader marker, so
this is a flagged adaptation, not a ported mechanism). Row-click
selection (SelectFellow) drives Dismiss/Leader targeting and the world
selection (SelectionChangeSource.Social).

D4: SocialPanelController now tracks "is the social window shown AND is
Fellowship the active tab" via UiTabPanel.ActivePageChanged +
OnShown/OnHidden, and calls SetPageVisible on every transition, which
sends 0x00A6 (idempotent, no-op while disconnected) -- the prerequisite
ACE gates its 0x02C0 vitals stream on.

Create flow: the inline name field (0x1000026F, an authored Editable
UiField -- live-DAT verified) gates the Create button's enabled state
exactly like retail (empty name = disabled = the whole refusal
mechanism, no separate error text); FellowshipShareXP's live value is
read at click time.

Actions + confirmations: Recruit/Dismiss/Quit/Disband/AssignLeader/
SetOpen all route through DeferredGameRuntimeStateCommands (new
Fellowship* methods) rather than a raw WorldSession send, so Quit
correctly picks up RuntimeFellowshipState's leader hand-off rule.
Button enable states port gmFellowshipUI::UpdateButtons verbatim. The
Open/Close button's caption swaps between the two DAT-resolved strings
cached once at Bind (never per-tick -- DatCollection is not safe to
touch unprotected from the render loop). RetailUiRuntime intercepts a
type-4 confirmation request before it reaches the generic
GameplayConfirmationController: IgnoreFellowshipRequests auto-declines,
FellowshipAutoAcceptRequests auto-accepts, neither set falls through to
the existing dialog machinery unchanged (D6).

D5 display: the per-fellow stats line uses retail's byte-decoded
even-split percentage table verbatim (1.0/.../.3111111/.28, default
0.0); the proportional branch omits the percentage rather than
inventing a formula (no acdream ExperienceToRaiseLevel table exists
yet). Both StringInfo variable substitution (row/stats/vitals text) and
ACCharGenData::FormatName (create-flow name canonicalization) are
unported prerequisites, so row text renders as plain numeric composites
-- register rows AD-80/AD-81 (docs commit).

D7: un-dims IgnoreFellowshipRequests/FellowshipAutoAcceptRequests
(consumed by the D6 auto-decline/accept) and FellowshipShareXP/
FellowshipShareLoot (consumed by Create + the page's own second
checkbox surface) on the Character tab -- 4 of 35 store-only rows
promoted to Live (31 remain dimmed).

Carry-forwards from the FA3 re-review, folded into this slice's
contract:
- UiTemplateListBox.FlushPreservingScroll -- preserves scroll offset
  across a rebuild instead of resetting to 0 (Flush's existing
  contract, unchanged, for Friends/Squelch).
- RowTemplateResolver -- the FA3 caching row-template resolver
  extracted from a MountSocialPanel local function into its own
  hermetically-testable class; now shared by Friends/Squelch/
  Fellowship's row families.
- Friends/Squelch scrollbars now resolve via the built
  UiTemplateListBox.ScrollbarElementId (DAT property 0x72) instead of
  a hardcoded literal, matching ConfigOptionsPageController's own OP6
  precedent.
- The Fellowship roster path never advances its revision latch on a
  partial resolver failure until the NEXT real membership change --
  never a per-frame retry loop.

Live-DAT verified (ACDREAM_PROBE_LIVE_MOUNT=1, extended
SocialPanelLiveMountProbeTests): the name field builds as UiField, all
11 buttons/checkboxes resolve, the row template's 5 checked fields
resolve to the right widget types, every checkbox label/tooltip and the
Open/Close captions resolve to real retail strings ("Open"/"Close"),
and a full production-path Bind() against live DATs produces zero
"not found" warnings.

App tests: +30 (7 UiTemplateListBox/RowTemplateResolver unit tests, 23
SocialFellowshipPageControllerTests covering roster diff/rebuild,
button enable rules, checkbox wiring, create-flow gating, D4
idempotency, and D5 formatting) plus 2 CharacterOptionsPageController
counts updated for the D7 un-dim (35->31 dimmed, 15->19 live).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-12 04:40:53 +02:00
Erik
ae77270939 fix(ui): fellowship checkbox count, row-text + ListBox Flush doc corrections, promote probe findings to assertions
Mechanism SHOULD-FIX 4: SocialFellowshipPageController's class doc said
0x1000026B holds "all three visible option checkboxes" — the fixture and
the decomp both authors FOUR (Ignore Fellowship Requests / Auto-Accept
Requests / Share XP / Share Loot, 0x10000270-0x10000273). Corrected.

Mechanism SHOULD-FIX 9: SocialPanelRowText.FindDeepest's doc promised
"the deepest UiText descendant" but the implementation returns the LAST
match in pre-order traversal order, which only equals the deepest when
the subtree is a single chain. Both real row templates ARE single
chains today, so behavior is unaffected — the doc now describes what the
code actually does instead of a stronger guarantee it doesn't implement.

Blast SHOULD-FIX 5: UiTemplateListBox.Flush()'s doc only mentioned the
ContentHeight reset; UiScrollablePanel.ClearContent() also resets scroll
position to 0, which the sibling UiItemList.Flush() (same method name,
different semantics) does NOT do. Documented explicitly, including the
UX cost this creates for a scrolled-in Friends/Squelch roster once its
scrollbar is wired (this fix round's blast MF-1) — flagged for whoever
revisits Friends/Squelch scrolling next rather than silently fixed as an
unasked behavior change.

Mechanism SHOULD-FIX 3: SocialPanelLiveMountProbeTests printed two
headline findings (the 0x10000492-authored-twice count, page exclusivity
after ActivateTabBehavior) without ever asserting them — a future
importer regression collapsing/dropping an instance, or breaking
exclusivity, could not fail this test. Both are now real assertions
(Assert.Equal(2, passupCount); exactly one page Visible and it is
Allegiance).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-12 03:44:05 +02:00
Erik
9afa05b563 fix(ui): FA3 fix-round — wire Friends/Squelch scrollbars, gate rebuild on visibility, fix revision-latch ordering
Campaign FA slice FA3 dual-lens review fix round.

Blast MUST-FIX 1: SocialFriendsPageController/SocialSquelchPageController
never wired their ListBox's own sibling scrollbar (Friends
0x10000517->0x10000518, Squelch 0x1000053E->0x10000543) — the two lists
had NO scroll driver at all (no wheel fallback exists on
UiScrollablePanel), making any roster past the visible extent completely
unreachable, not just awkward to scroll. Both controllers now wire
`scrollbar.Model = listBox.Scroll` scoped to their own page root, exactly
like the four existing UiTemplateListBox consumers
(Character/Chat/Config/KeyboardConfig). New tests prove the wiring AND
that a >panel-height roster is actually reachable through it.

Blast SHOULD-FIX 2/3: the Friends/Squelch resolver re-ran
LayoutImporter.ImportInfos (a full DAT tree walk) under the shared DAT
lock on EVERY row, every revision, even while the panel was closed —
RetailUiRuntime.MountSocialPanel's TemplateResolver now caches each row
template's ElementInfo the first time it is resolved and never
re-Imports for that template id again. SocialPanelController additionally
gates the Friends/Squelch Tick-driven rebuild on the panel's own
visibility via the (previously unused) IRetainedPanelController
OnShown/OnHidden hooks, so no DAT-locked rebuild work runs at all while
the panel is closed. SocialFriendsPageController/SocialSquelchPageController
also now only advance _lastRevision after every row resolves — a
transient resolver miss no longer latches an empty roster until the next
server-side change; it retries on the next Tick instead.

Mechanism SHOULD-FIX 8: SocialPanelController.Tick() now returns early
once disposed, matching every other J-slice teardown discipline (it was
being ticked unconditionally forever since RetailUiRuntime never nulls
the field).

Mechanism SHOULD-FIX 1: IsShowingAllegiance's doc claimed the F3/F4
close-on-second-press semantics were "retail's Toggle-action semantics" —
re-derived and confirmed NO retail OnAction consumer exists for either
action anywhere in the binary. Relabeled as acdream's own
OpenSpellbook-precedent convention; no register row added, following the
same no-row precedent OpenSpellbook and every other non-toolbar Toggle
panel already sets. Unknown filed as U11 in the panel-structure research
doc's §8 table.

Tests: 5 new (2 scrollbar-wiring pins, 2 long-roster-reachable-via-
scrollbar, 1 hidden-panel-does-not-rebuild/shown-panel-catches-up); 2
existing tests updated for the new visibility gate.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-12 03:43:35 +02:00
Erik
74c3d85d37 test(ui): FA3 -- social panel fixture dump, live-mount probe, conformance tests
- RetailLayoutFixtureGenerator dumps LayoutDesc 0x2100006E slot
  0x1000018F to the committed social_panel_2100006E_1000018F.json
  fixture, closing lane-A unknowns U3/U4/U6/U7/U10 with real DAT data
  (geometry/media/fonts/base refs, the fellowship empty/full frame
  containment, the panel/page P0x57 properties, the row-template arrays).
- SocialPanelLiveMountProbeTests exercises the PRODUCTION mount path
  against the live DATs (ACDREAM_PROBE_LIVE_MOUNT=1): the tab host
  resolves as UiTabPanel with its 4-entry table, all four pages resolve,
  the fellowship frame pair and allegiance signature elements resolve,
  0x10000492 is confirmed authored twice under the allegiance page, and
  every tab button caption is non-empty (the #375 resolver class).
- SocialPanelControllerTests pins the fixture-driven conformance: the
  real (coordinator-addendum-correcting) tab table and default entry,
  the Fellowship/Allegiance empty-state gates, Friends/Squelch row
  population and revision-driven rebuilds, and the D1 INERT-button
  contract (AD-79) via
  FriendsAndSquelchActionButtons_AreClickable_ButHaveNoHandler.
- RetailPanelCatalogTests gains the SocialPanel id/window-name/
  Mounted-not-Toolbar pins (lane A §6.1: no toolbar button).

Note: the ACDREAM_REGENERATE_UI_FIXTURES=1 run used to produce the new
fixture also touched keyboard_config_21000009.json and
options_2100002B.json on this machine (unrelated installed-DAT drift,
likely from local DAT-editing tooling) -- both were reverted to HEAD
before this commit; only the new social panel fixture is included.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-12 02:58:28 +02:00
Erik
1226f289f3 research: Campaign FA lanes A-D + the U2 slot-table probe (social panel found)
Four Opus research lanes for the Fellowship & Allegiance campaign:
panel structure, fellowship wire (two BN zero-folds broken by byte
decode: IsFull >= 9, the x87 XP-share table capping at 2.8x), allegiance
wire (27+5 messages binary-verified; tree assembly discard/reversal
rules; ACE zeroed-field caveats), and the acdream seams audit (H.2
scaffolding inventory, J-owner recommendation, AD-78 dimmed-row
inventory, bot-gate requirements).

Coordinator U2 closure (FaPanelSlotProbeTests, live DATs): Fellowship
and Allegiance are two of FOUR pages of ONE tabbed social panel — slot
0x1000018F, panel id 12, Type-8 host — alongside gmFriendsUI and
gmSquelchUI; lane A's separate-siblings mounting call is corrected in
its addendum, and the full 16-slot dump closes every unidentified
RetailPanelCatalog entry (Abuse/Book/LinkStatus/MiniGame/UA/Vitae/
Map+House/Journal).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-11 23:42:19 +02:00
Erik
d1c60df946 fix #382: chat-window indicator buttons invisible until first hovered
Root cause (found via reference-identity-verified live-DAT probing, not
a guess): the four main-chat-window indicator buttons (0x10000522-
0x10000525) resolve their own correct ActiveState="Normal" at
construction, then get blanked to "" moments later in the SAME
LayoutImporter.Build call. The indicator column's backing panel
(0x10000600) authors PassToChildren=true on its own empty DirectState
(confirmed live: States[0xFFFFFFFF].PassToChildren == true); when
LayoutImporter.BuildWidget's post-attach state reapply runs for that
panel, UiDatElement.TrySetRetailState cascades its DirectStateId to
every IUiDatStateful child, including the already-correctly-resolved
buttons. UiButton.TrySetRetailState's DirectStateId branch used to
accept that cascade because every button structurally carries a
DirectStateId entry in its States dict as a property bag (ToggleBehavior/
RolloverEnabled/etc), independent of whether it authors any blank
sprite, so TryFindState(DirectStateId) found that entry and blanked
ActiveState even with no "" media. A hover "fixed" it only because
UiButtonStateMachine.RequestedState resolves to the same canonical
Normal id regardless of PointerOver when RolloverEnabled is false.

Retail's own decompiled UIElement::SetState @0x00464e70 does the exact
same unconditional-commit-plus-cascade; retail avoids this specific bug
purely through construction timing (UIElement::Initialize's SetState
call precedes child-tree construction, so a cascade fired during import
always iterates zero children). Our port's LayoutImporter.BuildWidget
deliberately reapplies in the opposite order to give retained
PassToChildren tabs their authored child media, so this literal
state-machine port needed a compensating guard.

Fix: UiButton.TrySetRetailState's DirectStateId branch now requires
REAL "" media (HasStateMedia("")) before accepting the transition.
Scoped to UiButton only; UiDatElement's parallel branch and the cascade
mechanism are unchanged, so CharacterStatController's own
PassToChildren-driven chrome children are unaffected. Register row
AP-206 records the divergence from retail's literal unconditional-
commit semantics. Regressed by two fast unit tests in UiButtonTests.cs
(DirectStateCascade_WithoutRealMedia_DoesNotBlankAnAlreadyResolvedState,
DirectStateTransition_WithRealMedia_StillSucceeds) plus a live-mount
probe confirming all four buttons resolve ActiveState="Normal"
immediately after import against the real installed DAT.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-11 23:03:31 +02:00
Erik
a31fd631ad fix #381: Options-panel footer needs an opaque backing field
Root cause: a live-DAT probe found retail authors NO backing element
behind the Character/Chat/Config tabs' Apply/Reset/Defaults footer —
each page root has exactly five children (the row ListBox, its
scrollbar, and the three buttons) with zero direct-state media on the
root itself. Scrolled row content therefore bled through visibly
between/behind the three buttons; the bleed-through is a rendering gap
in our own composition, not a missing import.

Fix: new minimal widget UiSolidSpriteFill tiles
RetailChromeSprites.CenterFill (the SAME panel-background sprite the
Options window's own chrome already draws behind everything, not an
invented color) across the footer strip's rect, derived from the three
buttons' own resolved Top/Height and z-ordered strictly behind every
other child so it can never intercept input or occlude the buttons.
Register row AP-205 records the synthesis. Regressed by
OptionsPanelControllerTests.
Bind_SynthesizesOneOpaqueFooterBacking_PerPageWithApplyResetDefaults,
which pins exactly one backing field per page, sized from the live
button rects, z-ordered behind every sibling.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-11 23:01:43 +02:00
Erik
2a248c0d48 fix #380: Chat tab opacity sliders were missing their retail row captions
Root cause: PlayerOptionPage::AddSliderOption never sets a row's own
name-label text — the "Inactive Opacity"/"Active Opacity" caption comes
from a SEPARATE DAT-resident runtime catalog (DID 0x78000000, resolved
via the same two-level DBCache::GetDIDFromEnumStatic master-map/submap
lookup ChatOptionsDatDefaults already uses for enum 0x16/category 2,
here for enum 0x15/category 2) that nothing in the codebase ever
queried, so both slider rows rendered with no caption at all.

Fix: new ChatOptionsDatCaptions.TryRead resolves the DID-0x78000000
catalog's per-property name/tooltip entries (matched by the same
owning-property enum ChatOptionsDatDefaults already keys its defaults
by) and ChatOptionsPageController.BuildOpacitySliders stamps each
slider's own row caption/tooltip from it — falling back to no text
(never invented English) if resolution fails. Regressed by
ChatOptionsPageControllerTests.
Bind_WiresEachSlidersOwnRowCaption_FromTheResolvedDatCatalog and the
companion Bind_MissingCaption_RendersNoText_NeverInventsEnglish case,
plus a live-mount probe (OptionsPanelLiveMountProbeTests.
ProbeChatOpacityCaptions) confirming the production TryRead call
resolves "Inactive Opacity"/"Active Opacity" against the real DAT.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-11 23:01:22 +02:00
Erik
c121842664 fix #378: Config-tab dropdown menus render bare with no popup chrome
Root cause: DatWidgetFactory builds Config-tab Type-0x10000038 menu
leaves as bare UiMenu instances (matching the vendor/chat channel menu
pattern), but unlike those two controllers, ConfigOptionsPageController
never wired the menu's sprite/font/geometry properties after Bind — so
every dropdown rendered as plain text with no button well, no arrow
cap, and opened no popup on click (#374's fix only corrected click
ROUTING, not the missing chrome).

Fix: ConfigOptionsPageController.ApplyMenuChrome wires every Config-tab
menu row with the SAME retail sprite ids VendorUiController/
ChatWindowController's channel menu already use for this shared popup
catalog (LayoutDesc 0x21000043), verified against the live DAT via
OptionsPanelLiveMountProbeTests' ProbeConfigMenuChrome/
ProbeConfigMenuPopupChrome probes. Regressed by
ConfigOptionsPageControllerTests.MenuRow_SoundFeatures_OpensAndSelects
ThroughRealHitPath_UsingAuthoredPopupGeometry, which drives the real
click-to-open + item-pick path through the authored popup geometry.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-11 23:00:29 +02:00
Erik
a8ce010d02 fix(ui): #375 residual — activate the Configure Keyboard screen's own tab control
The gate-3 screenshot flight showed the keyboard list still rendering as
overlapping text after the string-resolver + parked-prototype fixes: all
SIX ActionClass pages were visible simultaneously (six stacked ListBoxes
— 'EmotesterSettings' is the Emote and CharacterSettings pages
interleaved) with six dead tab buttons above them. Root cause: OP8's
Bind built every page's rows but never called ActivateTabBehavior() on
the screen's own Type-8 tab host (0x1000049B), so no authored click
bindings were wired and no default-entry switch ran. The authored table
marks Movement (0x1000049D) IsDefault=true — activation now performs the
same default switch OptionsPanelController runs on ITS host, hiding the
other five pages and making the six tabs live.

Regressed by Bind_ActivatesTheTabControl_MovementDefaultShown_OtherPagesHidden
(fixture-driven: BehaviorActive, Movement visible, five pages hidden,
SwitchTo flips exclusivity).

Full Release suite: 13,087 passed / 4 skipped / 0 failed.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-11 16:11:22 +02:00
Erik
3441a71833 feat(ui): mark store-only option rows dimmed (user-directed, gate 2)
User directive (gate 2, verbatim): "mark all options that are not
implemented now, so I can clearly see what is not implemented." Store-only
rows keep full interactivity (still persist/send) but render their caption
in a shared dimmed grey (UiRenderContext.StoreOnlyCaptionColor, matching
the existing UiMenu.TextColorGhosted convention) instead of white/DAT
color. No invented marker text anywhere -- the dim IS the marker.

Config tab (ConfigOptionsPageController, 21 of 27 rows dimmed):
  Sound Features menu, Interface Sound trio, Play Sound Only When Active
  (AP-199); Screen Brightness, Automatic Degrades, Graphics Performance,
  Degrade Distance, the four Rendering Quality menus, Building Detail
  Textures, Multi-Pass Alpha (AP-198); Camera Stiffness, Camera Adjustment
  Speed, Align To Slope, Mouse Look Sensitivity, Invert Mouselook Y Axis,
  Use Mouse Turning (TS-74); Chat Font Face/Size (AP-200). NOT dimmed:
  Sound/Ambient trios, Resolution, Full Screen (LIVE), VSync and Field of
  View (NEXT-LAUNCH -- still implemented, just deferred to next process
  start, per the controller's own doc).

Character tab (CharacterOptionsPageController, 35 of 50 rows dimmed):
  every Group A (wire+store only) and Group D (deferred) row, plus the
  Group B rows the OP4 gate script's own step 16 confirms are unbound
  (ShowTooltips, SideBySideVitals, SpellDuration, AdvancedCombatUI,
  StayInChatMode, DisableMostWeatherEffects, PersistentAtDay,
  FilterLanguage, MainPackPreferred). NOT dimmed (15 rows): the six
  ListenTo*Chat ids (TurbineChatMembershipGate), DisableDistanceFog/
  DisplayTimeStamps/ToggleRun (bound at GameWindow.cs), the Group-C
  re-point (ViewCombatTarget/VividTargetingIndicator/CoordinatesOnRadar/
  AutoTarget/AutoRepeatAttack), and DragItemOnPlayerOpensSecureTrade
  (TS-48). Cross-checked against actual shipped consumers via source grep,
  not just the research doc's Group table, since OP4 only wired a subset
  of the doc's aspirational Group B.

Configure Keyboard (KeyboardConfigController): a row whose
RetailActionIdentityTable lookup fails (MappedAction null -- AP-203's
Emote/CharacterSettings set) dims its synthesized caption; the key
buttons stay fully bindable/persisted/conflict-checked.

Chat tab (ChatOptionsPageController): audited, zero store-only rows --
every filter block and both opacity sliders already have a live consumer
(ChatWindowState / RetailWindowOpacityController).

Ambiguity flagged, not guessed: the character-options-map.md research doc
lists AcceptLootPermits in BOTH Group A and Group C; its only code site
(LiveSessionRuntimeFactory.cs, the /consent command) is a second setter
for the same server bit, not a behavioral reader, so it is classified
Group A / dimmed here.

Register: AD-78 documents the convention (retail dims nothing; this is a
deliberate acdream-only divergence that retires as consumers land).

New per-surface conformance tests pin the exact dimmed/live set against a
literal expected list, so wiring a future consumer without also flipping
its row's literal fails the build:
CharacterOptionsPageControllerTests.StoreOnlyRows_MatchTheDerivationTableExactly
+ Bind_AppliesDimmedCaptionColor_ForStoreOnlyRows_AndWhiteForLiveRows,
ConfigOptionsPageControllerTests.CaptionDimming_MatchesTheStoreOnlySetExactly,
KeyboardConfigControllerTests.UnmappedRows_DimTheirCaption_MappedRowsStayWhite.

Build green; full Release suite 13,086 passed / 4 skipped / 0 failed
(baseline 13,082/4/0 -- delta is exactly the four new tests above).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-11 15:52:20 +02:00
Erik
8bd7e3b88d fix #375: Configure Keyboard live mount — string resolver + parked template prototypes
Campaign OP gate 2: the screen opened as a visual mess (textless
buttons/tabs, buttons above the window, overlapping text) while the
fixture conformance suite stayed green — the #372 class again. Two root
causes, both proven by the new env-gated live-DAT probe before fixing:

1. MountKeyboardConfig's main LayoutImporter.Build was the ONE mount in
   RetailUiRuntime not passing strings.Resolve — every AUTHORED caption
   (OK/Cancel/Defaults/Revert/Load/Save, the six ActionClass tab labels,
   the Command/Mapping column headers) built empty, while the
   controller's own resolveString row captions worked, which is why the
   screen was recognizable but textless. Fixed by passing the resolver
   like every sibling mount.

2. gmKeyboardUI authors its ListBox row templates (header 0x1000002E,
   action row 0x1000002F with the three 100x32 key buttons) as TOP-LEVEL
   siblings referenced by dat property 0x64. Retail never instantiates
   template-list elements as live widgets (AddItemFromTemplateList
   clones from the desc — the same re-import UiTemplateListBox's
   TemplateResolver performs), but ImportInfos built them parked at the
   screen's (0,0): three key buttons at y=0..32 ABOVE the framed panel
   (top y=62) — the 'outside the window' buttons — under a 570x40
   header text overlapping them and the top chrome. ImportInfos now
   skips top-level elements referenced by a SAME-LAYOUT template list
   (the same skip class as the existing BaseElement-prototype filter;
   same-layout only because element ids collide across layouts —
   0x10000211 is a page in BOTH the options and keyboard layouts).

The probe (ACDREAM_PROBE_LIVE_MOUNT=1) pins both against the real DATs:
prototypes absent from the built tree, and Defaults/Revert/OK/Cancel
resolving on the resolver-passing build. Post-fix the import collapses
to the framed 600x476 panel with every screen button inside its bounds.

Full Release suite: 13,082 passed / 4 skipped / 0 failed.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-11 15:27:30 +02:00
Erik
1c5cd969b4 Merge op8-keyboard: Campaign OP slice OP8 — Configure Keyboard
Brings b4edee97 (slice), b1968ce9 (M1/M2/M3 rework), f1d50207 (round-2
residuals). Review chain: REJECT -> rework -> REOPEN-narrow -> coordinator
third round; findings docs 2026-08-11-op8-review.md / -op8-rereview.md.
The merge lands OP8's six ListBoxes on top of 057d8cd7's #372 viewport
fix, which auto-heals the blank-pages hazard the re-review flagged — the
OP8 connected gate was contracted to run post-merge for exactly this.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-11 12:37:56 +02:00
Erik
f1d502072e fix(ui): OP8 round-2 residuals — inert-row conflict exclusion, DAT-default display, injectivity pin
R1 (code half): store-only rows (MappedAction null) are excluded from the
conflict universe — they never reach the InputDispatcher, so a chord they
display cannot collide; counting them made the ten Camera Alternate
arrow-key defaults trip a false N-way confirm on any arrow rebind. Mapped
cross-context sharing (retail's ConflictingMaps — the combat cluster)
remains deferred as ISSUES #373 with the OP8 gate script now carrying the
explicit do-not-file warning. SHOULD: unmapped rows with no persisted
chords display their DAT defaults (retail shows the arrow keys; blank
read as 'unbound') — display-only, the store is untouched until the row
itself is edited; the independence test updated to pin the new display
semantics while keeping its storage-isolation asserts. Injectivity of
RetailActionIdentityTable is now test-enforced (load-bearing for both M1's
per-row activation capture and M2's de-alias). R2: AP-203 addendum names
the ten same-verb-sibling-live rows and the conflict exclusion.

Full Release suite in this worktree: 13,155 passed / 4 skips / 0 failed.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-11 12:37:45 +02:00
Erik
348d794359 fix(ui): #372 — Chat filter labels resolve from their real table 0x2300000D
The 13 ID_ChatOption_TextFilter_* labels (and their _Desc tooltips) live
in string table 0x2300000D, not the 0x23000003 options table the section
headers and slider labels use. Dat-verified: the targeted sweep missed
(0x23000001-0A), the control key resolved ('Auto Target' — machinery
fine), and the exhaustive all-tables sweep (ProbeFilterLabelHome, now a
permanent env-gated probe) hit exactly once: 0x2300000D -> 'Combat'. The
initializer decomp confirms the hash KEYS are the symbol names verbatim
(the vftable-member operands at 0x006f04cd are the known pooled-string
artifact); only the research doc's table attribution was inferred rather
than dat-verified — corrected in §8.

All 13 rows now render their captions instead of the honest-fallback
blanks the first connected gate saw. Full Release suite green (one
Core.Net loss-simulation timing flake on the first run, green targeted
and on rerun).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-11 12:28:43 +02:00
Erik
b1968ce980 fix(ui): OP8 rework — activation/scope preservation, camera-row de-alias, conflict-confirm dialog
Fixes the three MUST-FIX findings from the 2026-08-11 combined dual-lens
review of commit b4edee97 (docs/research/2026-08-11-op8-review.md).

M1 — SetForAction destroyed ActivationType/InputScope on every write,
collapsing walk-mode's Hold, the three combat-scoped bindings, and
CameraInstantMouseLook's mouse chord the instant a row (including
Defaults, which touches all ~140 mapped rows at once) wrote back.
Widened the Bindings seam to carry the full Binding (chord + activation
+ scope), not a bare chord: KeyboardConfigController captures each
row's live Activation/Scope ONCE at build time (every multi-chord
action in KeyBindings.RetailDefaults() shares one pair across all its
bindings) and reapplies it on every write — rebind, Cancel/Revert, and
Defaults (which restores DAT-sourced KEYS only, never touches the
pair). New tests pin this across both Defaults and Cancel for a
Hold+MeleeCombat-scoped action.

M2 — InputMap 0x5 (CameraControls) and 0x6 (CameraAlternateControls)
aliased one InputAction each: both rows read/wrote the same live target,
so they showed identical stale chords, a rebind of one silently wiped
the other, and a row could conflict with its own twin. Building real
per-scheme dual-binding storage (or new InputAction members plus the
camera-dispatch code to consume them) is a feature, not a one-line fix.
Chose the third option: only ctx 0x5 — the scheme RetailDefaults()
actually has live support for — maps to InputAction; ctx 0x6 falls
through to the existing unmapped/store-only path (AP-203), fully
rendered, bindable, and persisted, honestly carrying no live effect.
This also retired 10 stale allowlist entries in the DAT-vs-
RetailDefaults() round-trip test: with the alias gone, ctx 0x5 alone
matches RetailDefaults() exactly for all twelve Camera actions.

M3 — the auto-reassign-on-conflict path was wired silent in production
(NotifyReassigned: _ => "") though the contract asked for a prompt and
retail confirms before overwriting (OpenOverwriteBindingDialog). Wired
a real confirm dialog through RetailDialogFactory.MakeConfirmation —
the same seam GameplayConfirmationController already uses — read
lazily since DialogFactory mounts after MountKeyboardConfig in
Initialize()'s order. Only reassigns on accept; decline leaves every
row untouched. AP-204 (which recorded the narrowing) is RETIRED; the
still-true OK/Cancel left-click-vs-right-click-release note moves to a
code comment (zero observable difference, doesn't warrant a register
row). Reverted the gate script's step 9 from documenting the silent
shape back to the real confirm-prompt behavior.

SHOULD-FIX addressed as one-liners in files already touched:
- S1: non-user-bindable conflicts are now checked BEFORE any row
  conflict (retail's own order), and ALL conflicting rows are collected
  (N-way), not just the first match.
- S3: Save wraps the file-write pair in the same try/catch
  RuntimeKeyBindingTarget.Apply already uses for keybinds.json.
- S4: assigning "Mapping 3" on a row with no existing bindings now
  lands on display index 2, not index 0 — ReplaceSlotValue trims only
  TRAILING empty slots instead of stripping every default(KeyChord).
  Right-click on an already-empty slot is now a no-op instead of
  shifting later bindings.
- S6: UiButton.OnRightClick returns false (unhandled, bubbles to
  parent) when no handler is set, disabled or not — matching the
  pre-existing behavior the class doc already claimed.

Left for a future pass (not one-liners): S2 (ActionMap.ConflictingMaps
is still unread — the conflict scan treats all 306 rows as one flat
universe instead of respecting the DAT's own legitimately-shared-key
table) and S5 (the ~330 DAT layout imports still run eagerly at mount
instead of lazily on first open).

19 KeyboardConfigControllerTests (was 12): +2 activation/scope
preservation (Defaults, Cancel), +1 camera de-alias, +2 confirm-dialog
accept/decline, +1 non-bindable-takes-priority-over-row-conflict, +1
sparse-row third-slot placement. Full solution suite 13,154 passed / 4
skipped / 0 failed (this round's baseline 13,147/4/0, zero regressions).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-11 09:53:10 +02:00
Erik
c3ed32fb5a docs+test: #372 — Options tabs blank + Gameplay buttons dead at first connected gate
The user's first Campaign OP connected gate (ACDREAM_RETAIL_UI=1, live
ACE) failed: Character/Chat/Config tabs render blank, only Exit Game of
the seven Gameplay buttons visibly acted. A live-DAT mount probe
(committed, env-gated) proves the panel BUILDS completely — root
UiTabPanel with a 4-entry tab table, all four page slots, all three page
ListBoxes as UiTemplateListBox with row templates, all seven buttons as
UiButton — and the three page controllers' Bind() run at mount. So the
defect is in the live render/input path the whole fixture suite never
exercises (mount -> ActivateTabs -> tab-click SwitchTo -> row draw /
button hit-test): the OP2-blast structural-false-negative class. Blocks
the OP3-OP6 gates; needs a dedicated debug slice + a gate-representative
test, not a guess.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-11 09:48:58 +02:00
Erik
b4edee970f feat(ui): Campaign OP slice OP8 — Configure Keyboard
Ports retail's Configure Keyboard screen (gmKeyboardUI, LayoutDesc
0x21000009) — its own separate full-screen window, not a fifth Options-
panel tab. Retires OP3's INERT contract for the Gameplay tab's Configure
Keyboard button (0x10000204).

DAT reader (src/AcDream.Core/Input/RetailActionMap.cs): reads the
ActionMap singleton (DID 0x26000000, empirically the only one — not
0x27000000 as GetDBOType's Turbine-internal tag would suggest) and both
MasterInputMap defaults (0x14000000 "gmDefaultMap"/0x14000002
"DefaultMap"), union-merged per (InputMapId, ActionId) — proven order-
independent since the two maps' one shared context (0x5) has disjoint
action-id sets. Empirically resolved three lane-D unknowns against the
live DAT: the six ActionClass values (1=Movement, 2=Camera, 3=UI,
4=Combat, 5=Emote, 7=CharacterSettings — 6 is genuinely absent), that
the six unnamed InputMaps are 100% non-bindable (render nothing, not an
unlabeled group), and that the enum-to-DID pairing for the two master
maps is inconsequential to the merge result.

Identity table (src/AcDream.UI.Abstractions/Input/RetailActionIdentityTable.cs):
maps DAT (InputMapId, ActionId) pairs to acdream's InputAction where a
live consumer exists (~140 of 306 user-bindable rows — Movement/Camera/
Combat map almost completely; UI/Quickslot/Chat partially; only 5 of 87
Emotes and none of 48 CharacterSettings hotkeys, since acdream has no
general emote player or hotkey-to-option-toggle dispatcher yet). Every
entry cross-verified by label match AND a DAT-default-vs-
KeyBindings.RetailDefaults() byte comparison (RetailActionIdentityRoundTripTests),
which caught a real off-by-one in the Quickslot 13-18 block before it
shipped and found three genuine pre-existing RetailDefaults() gaps
(walk-mode's Shift-echoed chord, ten CameraAlternateControls arrow-key
alternates, and the Quickslot Ctrl+N use-vs-select ambiguity) — none
introduced by this slice, all documented rather than silently patched.

KeyboardConfigController: six ActionClass list boxes built from the
DAT, merged with live KeyBindings for mapped rows (rebind applies
immediately through the same InputDispatcher every other input path
uses) and a new sibling RetailUnmappedKeyBindings store for rows with
no InputAction yet. Left-click a key button opens real InputDispatcher
modal capture; right-click erases that slot. N-way conflict detection
scans every other row plus the live KeyBindings table for acdream-only
actions (Ctrl+M mute, debug F-keys) as the non-user-bindable refusal
analogue, using retail's own byte-verified "Could not overwrite "
string (table 0x23000004). OK/Cancel/Defaults/Revert reuse the
OptionPage/IOptionRow verb model via a new ActionKeyMapOptionRow.
Persistence is keybinds.json only (D4 — no .keymap file interchange).

Five register rows: AP-202 (.keymap interchange narrowing), AP-203
(store-only rows with no live consumer), AP-204 (silent auto-reassign
instead of retail's confirm dialog; OK/Cancel ported as left-click not
right-click-release).

Small supporting additions: UiButton.OnRightClick (additive, no
existing behavior changed), InputDispatcher.Bindings getter (the
screen's single live-truth read seam), RetailScanCodeMap (DIK scan
code <-> Silk.NET Key, keyboard + the one mouse-device row).

19 new tests (6 ActionMap reader conformance incl. live-DAT row-count/
label pins, 1 DAT-vs-RetailDefaults round-trip, 12 controller
behavior tests against the committed keyboard_config_21000009.json
fixture) — full solution suite 13,147 passed / 4 skipped / 0 failed
(baseline 13,128/4/0, zero regressions).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-11 09:19:54 +02:00
Erik
472525b99e fix(ui): OP6 rework — six range captions, un-invert Sound enabled flags, five font faces
Fixes all three MUST-FIX findings from the OP6 REJECT review
(docs/research/2026-08-11-op6-review.md) plus its SHOULD-FIXes and NOTEs.

M1 — the "retail ships zero range captions" claim was a Binary Ninja
constant-folding artifact (the same class the header-string globals a few
lines above already worked around). The six SetSliderLabel call sites
byte-decode to reads of runtime-filled ID_Graphics_Value_* globals, not
immediate zeros (PE-byte-verified against the PDB-paired acclient.exe,
independently re-derived in this session, not just re-asserted from the
review). ConfigOptionsPageController.BuildSliderRow gained optional
rangeLowKey/rangeHighKey parameters wired for all six idx6 sliders (Camera
Stiffness Soft/Hard, Adjustment Speed Slow/Fast, FOV Narrow/Wide, Screen
Brightness Dark/Bright, Graphics Performance Speed/Detail, Degrade Distance
Close/Far) via the same SetRangeLabel mechanism OP5's Chat opacity sliders
already established. Mouse Look Sensitivity (idx3) correctly stays
uncaptioned — the one genuine SetSliderLabel omission. Class doc corrected;
gate-script lines 535/653-equivalent corrected in place.

M2 — the three Sound "Disabled" toggles were semantically inverted:
SoundManager::effect_sounds_enabled/ambient_sounds_enabled/
interface_sounds_enabled are all compiled = 1 in .data, and
UserPreferences::RegisterPreference binds the checkbox's boolean value
DIRECTLY onto those enabled-sense statics — checked-by-default means
enabled-by-default, not disabled. AudioSettings.SfxDisabled/AmbientDisabled/
InterfaceDisabled renamed to SfxEnabled/AmbientEnabled/InterfaceEnabled
(fresh JSON keys — the rejected slice's keys never shipped in an accepted
build); RuntimeSettingsStartupTargets.ApplyAudio now computes effective
volume through the extracted, independently-unit-tested pure function
ComputeEffectiveCategoryVolumes (enabled ? slider : 0f). This closes the
blast radius the review flagged: a missing key in an EXISTING settings.json
now falls back to AudioSettings.Default, which is enabled=true, so a fresh
launch is audible, not muted. AP-199's wording and gate-script step 6
corrected; the enshrined-inversion test rewritten to assert the correct
default and a new SettingsStore test pins the legacy-file fallback path.

M3 — UI_ChatFontFace now ships all five of retail's authored choices
(Arial, CourierNew, PalatinoLinotype, Tahoma, TimesNewRoman — a fixed
compile-time array at gmClient::InitUIPreferences, PE-byte-verified
present verbatim in .rdata, not a per-machine runtime enumeration as the
rejected slice's comment claimed). Default index 2 (PalatinoLinotype) now
indexes a real entry.

S1 — Bind() now emits the sixth trailing AddSeperator retail's own
InitOptions ends with (0x0049E80D), matching retail's 39-item ListBox (6
headers + 6 separators + 27 option-widget-rows) instead of 38.

S2 — Screen Brightness gets its own DisplaySettings.ScreenBrightness field
([-1,1], default 0) instead of overloading Gamma, which has a different
unit system (default 1.0, legacy [0.5,2.0] slider) and its own live
Settings-panel consumer.

S3 — UiScrollbar and UiMenu gained a settable TooltipText surfaced through
GetTooltipText (UiButton's existing pattern). Every slider and menu row's
own interactive widget (not just toggle/trio rows) now carries retail's
"<label>_Help" tooltip, verified as a universal suffix convention across
every AttachPreference site touched by this tab.

S4 — "800x600" added to DisplaySettings.AvailableResolutions: a genuine
retail display mode (Device::ForceDisplayResolution(1,0x320,0x258) at
startup) and the Config tab's own byte-verified Resolution row default, not
an invented preset. Defaults now lands on a highlighted, re-selectable
dropdown entry instead of an orphaned value.

S5 — four new/extended tests: ComputeEffectiveCategoryVolumes gets a
dedicated pure-function value assertion (Theory + a default-profile-is-
audible Fact) in RuntimeSettingsControllerTests, closing the "only event
order was asserted" gap that let M2 ship; a label/choice-key conformance
table in ConfigOptionsPageControllerTests enumerates every key this tab
queries (traced directly from the fixed code paths, not guessed) and fails
on an invented OR a dropped key; a per-row DefaultValue pin asserts every
row's default against the retail literal directly, independent of the
underlying settings-record defaults; and the S1 separator fix gets its own
39-item stacked-ListBox count pin.

NOTEs — AP-198's row count was always ten (its own enumeration never said
nine); the commit-message inconsistency N1 flagged is reconciled in both
the row and the section-summary line, and its Screen Brightness sub-clause
now matches S2. N2: Bind() now reads the scrollbar id from
UiTemplateListBox.ScrollbarElementId (dat property 0x72) instead of a
hardcoded constant. N3 (batch Defaults writes) and N4 (AfterApply on
Config-tab entry, needs no action) are left as recorded — out of this
rework's scope per the review's own disposition.

Full Release suite: 13,125 passed / 4 skipped / 0 failed (baseline
13,117/4/0 — net +8 tests added, 0 regressions, 0 removed).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-11 08:06:20 +02:00
Erik
6d0b0f9285 fix(ui): OP5 review fixes — thumb sync, batched opacity writes, cull register row, tests
Fixes the OP5 (Chat tab) dual-lens review findings against e71e5a96:

- M1 (MUST-FIX): each opacity row's own apply closure now pushes its OWN
  slider's thumb from the post-link truth (bindings.Current*Opacity()),
  mirroring the OP4 binding pattern. Before this, a single-slider drag
  followed by Reset reverted the live value/link but left that slider's
  own thumb stuck at the dragged position.

- S1 (SHOULD-FIX): the Chat tab's two opacity sliders no longer round-trip
  the whole settings.json on every drag MouseMove tick. UiScrollbar gains
  IsDragging + a DragCompleted callback (fires once, at the MouseUp that
  ends an actual thumb drag); the opacity apply closures flush immediately
  when not mid-drag (Reset/Defaults/discrete edits, same as before) and
  defer to DragCompleted otherwise, collapsing dozens of per-tick writes
  into exactly one per drag gesture. Live opacity still applies every tick.

- S2 (SHOULD-FIX): filed register row AP-201 and issue #371 for the
  UiScrollablePanel whole-row-cull-vs-clip divergence the review found
  (predates OP5, made user-visible by OP5's 240-260px filter blocks). Not
  fixed in this round (a renderer-level scissor stack is out of scope
  here) — corrected the OP5 connected-gate script instead so a straddling
  block's disappear-then-reappear-whole is no longer reported as a
  self-sizing regression.

- S3 (SHOULD-FIX): the chatWindowMainFilter round-trip test already
  existed in e71e5a96 (the review missed it scrolling past line 330);
  added the genuinely missing coverage instead — a composed test pinning
  RetailUiRuntime.MountChat's window-0 SettingsStore -> ChatWindowState
  seed (MountChat itself needs live DAT access and isn't unit-testable
  directly).

- N11: ScrollbarLinkage_ModelPointsAtTheChatListBoxScroll now asserts
  through the scoped page-slot lookup (UiElement.FindDescendant) instead
  of the flat layout.FindElement, which passed for the wrong reason given
  the shared scrollbar id 0x10000201 — matches OP6's own scrollbar-linkage
  test pattern.

Also updated ConfigOptionsPageControllerTests' local ChatOptionsPageController
Bindings fake for the new FlushOpacity parameter.

Full Release suite: 13,117 passed / 4 skipped / 0 failed (baseline 13,107/4/0
post-OP6 — 10 tests added, zero skips added, zero failures).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-11 07:36:54 +02:00
Erik
f5ac1742ba feat(ui): Campaign OP slice OP6 — the Config tab
Binds the retail Options panel's Config tab (LayoutDesc 0x21000029, 27
authored rows across 6 sections) through OP2's template mechanism and
OP3's per-page OptionPage model, matching the Character/Chat tab
controllers' established pattern.

The row table is transcribed directly from two decompiled sources —
gmConfigUI::InitOptions @0x0049E400 (row order, widget shape, defaults)
and gmClient::InitUIPreferences @0x004035b0 (the complete
UIPreferences::AttachPreference registration: every label/tooltip key,
every slider's real-unit range, every menu's enum choices) — which
resolves the research docs' own "U4" unverified slider-caption pairing:
retail ships ZERO range captions on this tab (every SetSliderLabel call
passes literal string id 0).

Consumer disposition: LIVE — Sound/Ambient volume-trio sliders and their
toggle halves (AudioSettings.SfxDisabled/AmbientDisabled now gate the
already-live engine write; RuntimeSettingsController.SaveAudio newly
pushes into OpenAlAudioEngine on every change, not just at startup),
Resolution/Full Screen (immediate window resize on save). NEXT-LAUNCH
(pre-existing precedent): Sync To Refresh, Field of View. STORE-ONLY
(register rows AP-198/199/200, TS-74 extended): Sound Features/Interface
trio/Play-Only-When-Active, the nine Graphics/Rendering-Quality rows
(Vulkan has no per-feature render knobs), Camera/Input's six rows and
Use Mouse Turning (no persistent mouse-turning camera mode), Chat Font
Face/Size (distinct new fields from the existing live ChatSettings.FontSize).

AudioSettings/DisplaySettings/CameraTurningSettings/ChatSettings each
gain new fields for their slice of the 27 rows, backed by SettingsStore
round-trips. A real bug caught by testing: the scrollbar scope lookup
used the standalone-layout root id (0x100001FF), which does not survive
base-merge into the host-mounted tree — fixed to scope from the tab
host's own page-slot id (0x10000213), matching Chat's established
pattern for the same shared-scrollbar-id hazard (0x10000201, authored by
both the Chat and Config ListBoxes).

30 new tests (27 authored rows register as 30 IOptionRow instances — the
three toggle+slider trios each register two). Full Release suite:
13,107 passed / 4 skipped / 0 failed (was 13,083/4/0 — net +24, the one
existing RuntimeSettingsControllerTests case updated for SaveAudio's new
live-apply call, not a regression).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-11 07:16:35 +02:00
Erik
ac0304dcf0 fix(ui,runtime): OP4 re-review residuals R1-R4 (coordinator pass) — OP4 CLOSED
R1: the timestamp prefix moves from ChatLog.Append (which stamped the
stored BODY, rendering 'Alice says, "13:05:09 hi"') to ChatVM's display
composition — FormatTimestampPrefix(entry.Received) prepends the COMPOSED
line, matching retail's separate-leading-string model (fprintf("%ls%ls",
ts, text) @0x00563e5b; AddTextToScroll receives composed lines). The
prefix renders entry.Received in LOCAL time (retail strftime), invariant
literal colons. The ten defect-pinning test cases across
ChatLogTests/RuntimeCommunicationStateTests are rewritten to pin the
corrected contract (stored bodies stay clean; the composed line carries
the stamp outside the quotes — ChatVMTests).

R2: open option-bearing panels converge on every PlayerDescription seed:
OptionPage.ReloadFromLive (per-row live re-read + gating re-eval, NO
AfterApply flush — the seed just cleared the dirty module),
OptionsPanelController.OnServerOptionsSeeded (active page),
CombatUiController.OnServerOptionsSeeded (SyncControls), wired through
RuntimeSettingsController.ServerOptionsSeeded from the same factory hook
LockUI already uses. Retail cannot reach this state (its panels close
across login); the adaptation exists because retained panels survive the
session boundary — documented at the seam.

R3: tests drive the refresh widget push (model AND checkbox converge) and
ReloadFromLive's no-flush contract. R4: AP-196 addendum names the
headless AutoRepeatAttack false->true effective-default flip and the
characterOptions escape hatch.

Full Release suite: 13,083 passed / 4 skipped / 0 failed.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-11 06:36:14 +02:00
Erik
e71e5a9614 feat(ui): Campaign OP slice OP5 — the Chat tab
Binds LayoutDesc 0x2100005C through OP2's template-list mechanism and
OP3's per-page OptionPage model: the General Options header + two
DualHash-linked opacity sliders (Option_DefaultOpacity_Property
0x10000080 / Option_ActiveOpacity_Property 0x10000081, live-apply on
drag through RetailWindowOpacityController, defaults read from the
installed DAT's DBProperties collection at DID 0x78000001 via
ChatOptionsDatDefaults), and the five per-window text-filter blocks
(main window 12 rows minus Gameplay, four floaties 13 rows each — the
byte-verified authored order cross-checked against the raw
gmChatOptionsUI::InitOptions/AddCheckboxBitfield64Option pseudo-C, not
just the research doc's own table) writing AcDream.Core.Chat.
ChatWindowState directly, the same state CH6's chat windows already
read.

AP-195 retired: ported both halves left open at the OP2 re-review —
the ALL-set LED media swap (new UiButton.FaceFileOverride, driven by
the block-level P0x10000082/P0x10000083 sprites now threaded through
ElementInfo/DatWidgetFactory) and the CreateChildren self-sizing tail
(UiCheckboxBitfield64.Height grows with its stacked row content; the
enclosing ListBox reflows around the block's FINAL height via the new
UiTemplateListBox.AddPrebuiltRow, reusing the ListBox's own stacking
rather than a third stacking path). AP-187 broadened to cover the main
window's own filter (previously only the four floaties) and the new
live-editing write path.

The main chat window's filter (retail window id 8, ChatWindowState id
0) gains its own settings.json persistence (ChatSettings.
ChatWindowMainFilter) alongside the pre-existing floaty 1-4 fields;
opacity persistence is now wired on every live slider change, not only
through the old dev-scaffold Settings panel.

Fixture regeneration (ACDREAM_REGENERATE_UI_FIXTURES=1) picked up the
new ElementInfo.LedCheckedSprite/LedUncheckedSprite fields across all
19 committed layout fixtures — purely additive, confirmed against the
live installed DAT (0x10000520's own 0x82/0x83 properties resolve to
0x06004D17/0x06004D19 exactly as AP-195 documented).

Conformance: FilterRows/FilterBlocks pinned against the byte-verified
authored order and ChatWindowState's own default constants; the AP-195
LED swap and self-sizing behavior; the DAT opacity-default extraction
against the live installed DAT; live filter/opacity writes reaching
ChatWindowState/RetailWindowOpacityController; OnShown re-seed and
Reset/Defaults ghosting per the OP4 binding-pattern discipline.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-11 06:25:59 +02:00
Erik
bc43fb1d1d fix(ui,runtime): OP4 review fixes — live re-seed, enable-gating, Combat panel re-point, universal timestamps
Both OP4 reviews converged on one headline bug (Character-tab rows never
re-read live server truth after their pre-login constructor-word seed) plus
overlapping MUST-FIXes. All ten converged/consolidated findings land here:

MUST-FIX:
- BoolOptionRow.SaveCurrentValue now re-reads its live binding (retail's
  GetValue()-into-SaveCurrentValue) on every OnShown — panel open, tab
  switch in, initial activation — instead of trusting the pre-login
  constructor word it was built with. Reset/tab-switch can now only
  restore values that were actually live at the last show. LockUI's
  host.Root.UiLocked one-shot mount seed now also converges on every
  PlayerDescription via the existing OnCharacterOptionsChanged hook.
- Apply/Reset are wired to OptionPage.OnOptionChanged in production
  (Ghosted when nothing changed, Normal when dirty, run once at bind so
  both start disabled per retail's PostInit); Defaults stays ungated.
- The Combat panel's three LEDs (Repeat Attacks/Auto Target/Keep in View)
  now read/write the same RuntimeCharacterOptionsState seam the Character
  tab uses instead of a disconnected client-local GameplaySettings copy —
  closes the "two writable copies" divergence. The three now-orphaned
  GameplaySettings fields and RuntimeSettingsController's mirror
  properties/SetCombatGameplay are deleted outright; the headless host's
  hardcoded AutoRepeatAttack/AutoTarget now read the live option bit.
- RuntimeSettingsController.SetUiLocked's convergence guard now compares
  against the last value actually applied to the runtime target instead
  of the persisted GameplaySettings.LockUI snapshot, which could already
  match a server-derived request without ever having been pushed.

SHOULD-FIX:
- DisplayTimeStamps now prefixes every chat producer (ChatLog.Append is
  the one seam all of them funnel through), not just AddText's own
  callers — heard speech, emotes, Turbine channels, and combat text were
  previously missed. The prefix format escapes its colons and forces
  InvariantCulture instead of the culture-dependent TimeSeparator
  placeholder.
- sky.frag now honors uFogParams.w (fog mode) like the mesh/terrain
  shaders, so Disable Distance Fog stops the sky dome's horizon band from
  blending toward fog color too.
- Corrected the "byte-verified" overclaim on the timestamp format string
  doc comment (BN-sourced, wire doc U6) and the AP-194 anchor-column
  class-name typo; the RunAsDefaultMovement doc comments now cite retail's
  actual acclient.h enumerator name.
- Added: DispatcherMovementInputSource's option x modifier truth table
  (incl. || AutoRunActive with the option off), the per-page Apply/Reset
  enable-gate tests, a real checkbox.OnClick/ToggleBehavior-driven click
  test, and hash-pins for the six header string keys.
- Gate script step 8 corrected for the logout-flush false-failure
  (closing the panel before relogging is load-bearing); a new step
  documents the enable-gate sequence and the Combat-panel/Character-tab
  cross-check.

Register: AP-196 (the Group-C default-source change + GameplaySettings
retirement) and AP-197 (the ignored per-character timestamp format
override) filed in this commit.

Full Release suite: 13,044 passed / 4 skipped / 0 failed (was 13,008/4/0;
net +36 tests from new coverage and legitimate assertion updates from the
GameplaySettings retirement).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-11 05:30:26 +02:00
Erik
22b86b9ff4 feat(ui): Campaign OP slice OP4 — the Character tab
Binds LayoutDesc 0x21000028 (gmCharacterSettingsUI) through OP2's
template-list mechanism and OP3's OptionPage model: 6 authored group
headers + 50 toggle rows (49 from the 2013 build + D3's "Listen to PK
death messages", AP-193) in research doc §2's authored order, each row
resolved by PlayerOption id through CharacterOptionTable, seeded from
live RuntimeCharacterOptionsState, defaulted from CharacterOptionTable.
ClientDefault (byte-verified against UIOption_Checkbox::SetPlayerOption
@0x00486e80's own GetDefaultOptionValue call — AP-194 updated to confirm
the directive was followed), labels/tooltips resolved by name from
string table 0x23000003 (never hard-coded English), and registered with
OptionsPanelController.CharacterPage. Apply/Reset/Defaults
(0x100001FC/FD/FE) are now wired per-page via a scoped subtree search
(UiElement.FindDescendant, promoted from UiTabPanel) since Character/
Chat/Config each author their own physical instance under the SAME
element ids.

Consumers: Group A (29 ids) wire+store only via the existing
SetSingleCharacterOptionRuntimeCmd/TrySetOption seam. Group B: Display
Timestamps prefixes new transcript lines (RuntimeCommunicationState.
DisplayTimestampsSource); Disable Distance Fog forces FogMode.Off
(WeatherSystem.DisableDistanceFogSource, retiring half of TS-73); Run as
Default Movement inverts the walk-mode modifier's default
(RuntimeLocalPlayerMovementState.RunAsDefaultMovementSource). Group C
re-points AutoTarget/AutoRepeatAttack/ViewCombatTarget
(CharacterOptionCombatSettingsSource), VividTargetingIndicator/
CoordinatesOnRadar/LockUI/AcceptLootPermits from the client-local
GameplaySettings record to the canonical server bit — closing two
previously-unfiled divergences where AutoRepeatAttack and
AcceptCorpseLootingPermissions never reached the wire despite being
retail auto-save ids. TS-73 narrowed to its two still-open cases;
TS-75..TS-80 file the genuine gaps (no day/night force, no weather-
particle/profanity-filter/salvage/housing/pickup-preference subsystem,
fellowship-create's unaudited client-sourced field) rather than
inventing stand-ins.

Conformance: CharacterOptionsPageControllerTests pins all 50 rows
against CharacterOptionTable in both directions (an invented or dropped
row fails the build), the authored group/order row-by-row, and the
build/seed/Apply/Reset/Defaults/wire-publish behavior end-to-end against
the committed fixture. 52 new tests; full solution suite 13,008 passed /
4 skipped / 0 failed (was 12,956/4/0).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-11 04:31:34 +02:00
Erik
cb3346907d fix(ui): OP3 re-review residuals R1/R2/R3 (coordinator pass)
R1: the gate script no longer promises a timestamp prefix on the Magic
macro lines — acdream renders no chat timestamps yet (the Display
Timestamps consumer is OP4 scope; no chat-log file exists, TS-69). A
bare light-blue transcript line is the CORRECT gate outcome.

R2: IsGrounded yields null (silent) for a NULL controller in player
mode — the prior pattern returned false and fired the mid-air refusal
retail cannot produce in that state; comments now match the code.

R3: the dormant-ActivePageChanged pin now applies the real stimulus —
every authored tab button on a dormant host must carry NO click handler
(RetailTabBinding.SetClick never ran), which is AD-73's actual dormancy
mechanism; SwitchTo deliberately has no guard.

OP3 is CLOSED: dual APPROVE-WITH-FIXES -> fix round 386076af ->
re-review REOPEN(narrow) -> this pass. Connected gate now READY.

Full Release suite: 12,956 passed / 4 skipped / 0 failed.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-11 03:25:40 +02:00
Erik
386076af0f fix(ui): OP3 review fixes — byte-verified Magic chat lines, Gameplay/OptionPage shape, mid-air tri-state, shared geometry
Consolidated fix round for the two OP3 dual-lens reviews
(docs/research/2026-08-11-op3-review-{mechanism,blast}.md), both
APPROVE-WITH-FIXES.

MUST-FIX:
- The six "Use Mouse Turning Settings" chat lines were typed
  RetailLogTextType.ClientLocal (0x1A); retail types them 0x07 (Magic).
  BYTE-VERIFIED against the PDB-paired binary at all six
  gmConfigUI::SetMouseTurningDefaults call sites (0x0049E972/E9E2/EA52/
  EAA4/EAF6/EB48): every site pushes `6a 07` (type=7) immediately before
  the text-pointer push and the AddTextToScroll call. Added a dedicated
  OptionsRuntimeBindings.DisplayMouseTurningMacroLine seam routed at
  Magic (scrolling chat transcript, light blue, timestamped) instead of
  the 4-slot SpewBox ClientLocal uses; the mid-air refusal and UA/RA
  keep ClientLocal (both independently confirmed correct).
- Filed AD-77: the client-wide floating-only gmPanelUI host divergence
  (retail also exposes a docked 0x21000017 host) the plan §5 delegated
  to this review, scoped to every main panel, not just Options.

SHOULD-FIX:
- gmGameplayOptionsUI is not an OptionPage in retail (acclient.h:55857,
  UIElement_Field). OptionsPanelController now constructs the Gameplay
  slot's OptionPage with AfterApply deliberately null, so entering/
  leaving that tab never publishes SaveCharacterOptionsRuntimeCmd.
  Corrected OptionPageModel's doc comment and rewrote the two tests
  that pinned the wrong (Gameplay-flushes) shape.
- Added the OptionPage.OnOptionChanged seam (PlayerOptionPage::
  OnOptionChanged @0x004F27D0) — fires as the last step of Apply/
  Reset/Defaults, plus once per live LED edit via a new
  IOptionRow.AttachPageNotify hook (BoolOptionRow wires it into
  SetCurrentValue only, matching retail's Apply(1)-only
  HandleDialogAndNotices path). OP4-6 will bind Apply/Reset enable
  state to this.
- Exit to Character Selection's mid-air refusal is now tri-state
  (Func<bool?> IsGrounded): retail's UseTime only reaches the airborne
  test inside `else if (smartbox->player)`, so outside player mode (or
  with no live controller) the button is a SILENT no-op, not a
  refusal. Fixed the inverted comment at both call sites.
- Options panel geometry now matches its nine gmPanelUI siblings
  sharing RetailPanelUiController's one main-panel rectangle
  (ResizeX=false, bottom-edge-only resize, no invented Min/MaxWidth/
  Height) instead of being the only all-four-edge/horizontal-resize
  outlier whose width silently reverted whenever a sibling was shown.
- Added the three missing test pins: Options/Character mutual
  exclusion through a REAL RetailPanelUiController registration,
  RetailDialogFactory.MakeConfirmation's omitted-queueKey overload
  sharing DefaultQueueKey, and UiTabPanel.ActivePageChanged never
  firing on a dormant (non-activated) host.
- TS-74's What/Where now names the five store-only CameraTurning
  preferences explicitly instead of only mentioning them in Risk.
- Test script gains the toolbar-button ghosted->enabled+highlight
  check, UseMouseTurning-survives-relogin and the five prefs-survive-
  relaunch steps, a UA/RA legibility eye-item, and the corrected
  bottom-edge-only geometry description for step 5.

One-liners fixed in files already touched: symmetric close-button
resolve-failure logging in OptionsPanelController.Bind (blast NOTE 8).

Full Release suite: 12,947 passed / 4 skipped / 0 failed (baseline
12,935/4/0 post-OP7 — 12 net new tests; the two OptionPageModelTests
"wrong-shape" tests were renamed/rewritten in place, not removed).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-11 03:05:27 +02:00