Third iteration on the screen, completing AD-98. The previous substitution
stretched only the root BACKGROUND while the child widgets stayed at their
authored 800x600 pixel positions - and the background painting carries
visual anchors (the World/Characters captions are art), so the user gate
showed captions overlapping the listbox and every widget misaligned
against the stretched art.
Retail model (established at 71bf24fb): fixed-canvas pre-world screens
render at authored 800x600 and the whole composed frame stretches once at
presentation; the blitter has no stretch mode. Our equivalent now does the
same one stage earlier:
- UiRoot.FixedCanvasSize: while the char-select screen is active, the
retained tree lays out in its authored canvas and Draw scopes a uniform
scale onto TextRenderer.CanvasScale; the mouse entry points apply the
exact inverse so MouseX/MouseY and every hit test live in canvas space.
- TextRenderer.AppendQuad is the single emission chokepoint - sprites,
rects, AND glyphs scale together, including retail-authentic non-uniform
aspect distortion and stretched text. World-space HUD stays native (the
scale resets outside UiRoot.Draw).
- CharacterManagementUiController stops resizing Root to the viewport;
activate/deactivate/dispose set and clear the host canvas.
- UiDatElement returns to retail-pure copy-or-tile; the interim
StretchOwnBackgroundToFill flag is deleted.
- AD-98 updated to describe the completed substitution.
Tests: canvas-scale quad math, inverse input mapping (window click lands
on the canvas-space widget), degenerate-size guards, controller keeps
authored extent + sets/clears the canvas. App suite 5085/6 skips; live-DAT
char-select probes 3/3.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The LA8 char-select root (0x1000039A) authors LeftEdge=TopEdge=RightEdge=
BottomEdge=0 ("no anchor") in the installed DAT — confirmed via the new
CharacterManagementLiveDatTests.RootAuthorsNoEdgeAnchors_RetailNeverResizesItSelf
gate — so retail's own UIElement::UpdateForParentSizeChange (0x00462640) never
resizes this element; it stays a fixed 800x600 rect in retail's own tree.
Retail's generic UI sprite blit, Graphic::Draw (0x00693b20) dispatching to
Graphic::PutImage (0x00693a30) for an exact/undersized destination or a
modulo-wrapped tile loop otherwise, has no third "stretch" mode — confirmed
against BlitMode (acclient.h ~3135) and MD_Data_Image::m_drawMode/DrawModeType,
both COLOR-blend selectors, not tile-vs-stretch geometry modes. The prior
"Normal -> tile, matching ImgTex::TileCSI" citation in UiDatElement was a
mis-attribution: ImgTex::TileCSI (0x0053e740) is called exclusively from
TexMerge::CopyAndTile/ImgTex::CopyCSI for LAND-SURFACE terrain texture
compositing, never from the UI element system.
Given the dat authors zero resize anchors and the blitter can only copy or
tile, the only way retail's whole pre-world scene (background + buttons +
listbox together) fills an arbitrary window resolution is that these
fixed-canvas "flow" screens render at 800x600 and the WHOLE FRAME is
stretched once at presentation — outside the UI sprite system entirely.
acdream has no offscreen fixed-resolution UI render target / present-time
scale pass; CharacterManagementUiController's constructor instead resizes
the MOUNTED ROOT element itself to the live viewport, which is why its own
background tiled (Width/tw > 1 at any resolution above 800x600, wrapped by
GL_REPEAT).
Fix: UiDatElement gains StretchOwnBackgroundToFill (default false, every
ordinary chrome/container element keeps tiling) — when set, the element's
own DirectState background draws as one UV-0..1 quad instead of the native
tile formula. CharacterManagementUiController sets it on Root right where
Root is resized to the host viewport, reaching the same visual result as
retail's present-time stretch (no tiling, no aspect-preserving letterbox)
through a different mechanism. Divergence register row AD-98 records the
substitution.
Tests: three new UiDatElementTests pin the UV-span mechanism generically
(tile past 1.0 when unset and rect exceeds native size; clamped to 1.0 when
set; byte-identical to the old tile formula when rect equals native size,
so every unaffected panel is untouched). CharacterManagementUiControllerTests
pins Root.StretchOwnBackgroundToFill == true post-construction. The live-DAT
gate confirms the root's zero edge-anchors and Type=3 against the installed
DAT. AcDream.App.Tests: 5084 passed / 3 skipped with ACDREAM_PROBE_LIVE_MOUNT=1
(5081/6 skipped without it — the 3 live-DAT-gated tests skip).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The Opus retail-lens review decoded the PDB-paired binary at
CPlayerSystem::RestoreCharacter@0x0055d760 and refuted the
uninitialized-edx justification: the two extra arguments are real
push imm32 of a constant PStringBase (BN mis-renders them, but they
pack to >=4 bytes each), so retail 0xF7D9 is >=16 bytes where ours
is 8. The guid-only CODE stands (ACE reads only the guid; holtburger
consensus) but it is an adaptation, not a corrected decompile — filed
as divergence register AD-97 and the doc comment now states the true
mechanism.
Also from the review: the 0xF643 conditional-parse doc now names BOTH
ACE flag-only failure branches (NameInUse + Corrupt); CharacterError
0x08 doc corrected (ACE misnames it ServerCrash2 — the port corrects
an ACE misnaming; ACE omits three values, not four); LA7b hazard notes
added (ACE silent no-reply on unknown restore guid; retail SendToLogon
vs SendToControl routing; NumErrors never rendered); two review-nit
tests (flag=0 Undef flag-only, non-Ok body with trailing bytes
ignored).
Core.Net suite: 953 passed / 0 failed.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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>
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>
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>
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>
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>
position memory, unified monitor, maximized restore; AD-92
Dual-lens Opus review of e56aa511 (reports committed under
docs/research/). The consolidated corrections:
- Mechanism M1 (load-bearing): on Windows, Silk's GLFW error callback
QUEUES exceptions on a static list instead of throwing - they detonate
later at window close, which is exactly #388's original two-stage
crash shape. catch(GlfwException) was dead code here and a failed
SetWindowMonitor "succeeded". Success is now judged by the NATIVE
POST-CONDITION (GetWindowMonitor after the call) on both enter and
exit; the catches remain only for the throwing platforms.
- M2 (both lenses): same-mode fullscreen re-apply is a no-op BEFORE any
native work (new IDisplayModeSwitcher.CurrentFullscreenMode). Every
Display-backed Config row applies per change - sliders per DRAG TICK -
so without this every tick while fullscreen re-issued a real
display-mode change.
- M3/M5 (both): the remembered windowed placement is process state (two
target instances exist - startup and live-save); a fullscreen boot now
exits through either instance to the real placement, not the (60,60)
literal.
- M4 (both): the switcher resolves the WINDOW'S monitor (attached
monitor when fullscreen, else IWindow.Monitor's index into the GLFW
array - the same monitor DisplayModeCatalog enumerated), primary only
as a last resort; the offered-list/switch-target mismatch is gone.
- Blast M2b: the offered-mode validator falls back to the SAME static
ladder the dropdown falls back to - Full Screen is no longer a
permanent silent no-op on catalog-less hosts (the switcher's own
monitor-mode-list check remains the hard guard).
- Blast M3: a windowed pick on a MAXIMIZED window restores it first
(Size writes are silently ignored while maximized; the deleted
WindowState=Normal write used to do this incidentally). New
IWindowedSizeSurface.IsMaximized/Restore.
- Mechanism M5: no silent bail-outs - the unparseable-resolution
fullscreen path logs, and the failure line no longer claims "staying
windowed" when the state is unchanged (#392 noted inline).
- Q1 nit: one cached Glfw wrapper (per-call GetApi allocated + took a
native refcount); IsFullscreen/CurrentFullscreenMode guarded.
- AD-92: highest-refresh-for-WxH + refuse-and-log versus retail's
pass-through-and-error ForceDisplayResolution.
Known-open tail, filed not hidden: #392 (persisted-flag divergence on a
refused enter - needs an apply-result seam); the mechanism report's
pacing-refresh WATCH rides the same seam.
Tests: +3 (same-mode no-op, unparseable-while-fullscreen refusal,
maximized restore-before-write). App suite 4,975/3 skips.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Decomp-first per the block's rule: the research doc
(docs/research/2026-08-13-retail-ui-display-change.md, committed here)
pulled retail's actual mechanism before any code. A display change runs
UIElementManager::RefreshEvent @0x0045C530 ->
UIElement::UpdateForParentSizeChange @0x00462640, which unconditionally
re-applies every floating window's own clamping MoveTo override
(x = max(0, min(x, parentW - selfW)) - top-left priority, oversized
windows pin to 0), then broadcasts global message 0xE whose sole
listener reloads the per-resolution auto layout. No proportional moves,
no resets; retail saves layouts only via @saveui.
Port: RetailWindowLayoutPersistence.ClampAllToScreen() is the cascade
clamp (no store I/O; _restoring suppresses the per-move save so a live
drag-resize cannot write settings.json per frame), and
RetailUiRuntime.Draw carries a two-step screen-size edge detector:
change frame -> clamp; first stable frame -> one
RestoreAll(saveBack:false) per-resolution reload (the 0xE analog; no
lazy save-back, matching retail's save-only-on-command). The login
restore path already used retail's exact clamp math (Apply) - the live
trigger was the missing half, which is precisely the stranding the user
reported.
Deliberate deviation, register AD-91: retail's gmFloatyChatUI windows
have NO clamp and can strand; the block's requirement ("UI windows must
stay reachable") clamps every registered window uniformly.
Tests: 5 new persistence facts (clamp/top-left-pin/no-move/no-save-on-
clamp/no-save-on-live-reload). App suite 4,967/3 skips. Gate script
section D3 filled in.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Dual-lens Opus review of 7e0c1303 (reports committed under
docs/research/). The law, gate, and vertical application are CONFIRMED
at instruction-byte level against the PDB-paired acclient.exe (the BN
text FPU-elides this whole area); the fix round addresses the findings:
- Blast MUST-FIX 1: real schema migration instead of a hand-edited dev
file. SettingsStore v2->v3: a pre-v3 display.fieldOfView was the
applied vertical FOV in degrees; v3 means retail's m_fGameFOV.
LoadDisplay migrates on read - the untouched old default 60 maps to
the retail default 90; a deliberate other value preserves its visible
16:9 framing (x (16/9 - 0.1)), clamped to the registered [10,160];
the next save stamps v3 and migration never reruns. The dev
settings.json hand-edit was reverted so the migration owns it.
- Blast MUST-FIX 2 / mechanism M2: the Field of View now applies LIVE on
Save (retail: Render::GRPCallback_OnRenderPreferenceChanged @0x0054d999
-> SmartBox::SetDefaultFov). RuntimeSettingsTargets gains the camera
graph and applies through ApplyDisplayWindowState - the update-phase
seam, deliberately NOT the render-phase preview path (the review's
WATCH-3 cull-vs-raster landmine).
- Mechanism M1 -> register row AD-90: retail's divisor aspect runs
through the Render.AspectRatio preference (ComputeAspectForViewport
@0x0054f150, (w/h) x pref x 0.75) - exactly raw w/h at the registered
default, which is what acdream assumes; retail's NaN-through-the-gate
quirk (M3) is folded into the same row as deliberately not reproduced.
- Docs: RetailFieldOfView now cites the decisive vertical proof
(D3DXMatrixPerspectiveFovLH fovy slot @0x0059ab71), the unconditional
SmartBox::RenderNormalMode site, and M4's exact horizontal numbers
(89.0/83.9/80.6 deg); the Config FOV row comment updated to LIVE.
- Blast WATCH 4 disposition: the 15 replay-harness PI/3 constants stay -
they are CAPTURE-TIME camera parameters for recorded fixtures, not
production framing; changing them would invalidate the replays.
Tests: +6 SettingsStore migration facts, +1 live-apply fact.
App suite 4,962/3 skips; UI.Abstractions 922.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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>
Retail's world-camera FOV is not a constant: the applied vertical FOV is
m_fGameFOV / (viewportAspect - 0.1), recomputed on every aspect or
game-FOV change (CreatureMode smartbox sites 0x00452b2f/0x00453b14),
gated by Render::SetFOVRad's open (0, pi) acceptance (0x0054b2d0 -
rejected results keep the previous FOV). m_fGameFOV defaults to pi/2 =
90 degrees (0x00454649) and is what the Field of View option sets in
degrees (0x00451e6a; registered range [10,160] default 90 -
gmClient::InitUIPreferences @0x004035b0). Net effect: the horizontal
view stays ~85-90 degrees across aspect ratios; wide screens trim the
vertical slice instead of ballooning the sides.
acdream hardcoded FovY = pi/3 = 60 degrees on all four world cameras,
aspect-independent, and the Config slider wrote raw vertical-FOV
degrees. New: RetailFieldOfView (the law + gate, decomp-cited),
CameraController.GameFovRadians + SetGameFov + one ApplyProjection
chokepoint recomputing every camera on SetAspect/SetGameFov/
EnterChaseMode/RestoreState; ApplyFieldOfView now feeds the law;
DisplaySettings.Default.FieldOfView 60 -> 90 (the retail registered
default; the stored number changed MEANING with this commit).
The same seam closes a second latent bug the 2026-08-13 "squished" gate
report exposed: SetAspect only ever updated Orbit/Fly - the CHASE
cameras (the ones the player looks through) kept their creation-time
aspect across every mid-session resize, drawing the world at the old
shape stretched onto the new viewport.
The paperdoll camera stays outside the law by design (retail portrait
mode is UseSharpMode, not smartbox - DollCamera's own doc).
Tests: RetailFieldOfViewTests (golden law values at 4:3/16:9/21:9, the
constant-horizontal property, the rejection gate, controller propagation
incl. chase attach/restore + rejected-law aspect-still-propagates);
DisplaySettingsTests + RuntimeSettingsControllerTests updated to the new
semantics. App suite 4,953/3 skips; UI.Abstractions 916/0. AD-89 retired
in this commit; user settings.json migrated 60->90 by hand (stale
pre-port default).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Both from the 2026-08-13 display gate session. #389 carries the full
decomp-verified retail FOV law; #390 requires the retail reposition
mechanism from the decomp before any clamp is implemented.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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>
The retail four-tab social panel (Fellowship & Allegiance) is
code-complete: all six slices landed and reviewed (dual-lens Opus review
-> fix round -> narrow re-review each). Fellowship two-session flow proven
live (FA6 bot gate PASSED). Closeout bookkeeping:
- register AD count 66 -> 67 (AD-87, the deferred allegiance bot gate);
- plan status flipped to CODE-COMPLETE with the OWED connected gates +
#384 (allegiance-swear ACE non-response) called out;
- CLAUDE.md Current-state gains the Campaign FA paragraph
(per feedback_claude_md_staleness), pointing at the memory digest.
Owed: the user's connected gates (§FA3-§FA6 of
docs/research/2026-08-12-campaign-fa-test-script.md) and #384's
ACE-console disambiguation. Full suite 13,304/4/0.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
docs/ISSUES.md #384 records the live-run evidence trail (six connected
runs, the 0.005 m distance diagnostic, the confirmation-arrival diagnostic
that never fires) behind AllegianceGateEnabled=false.
docs/architecture/retail-divergence-register.md AD-87 records the honest
divergence this deferral creates: the allegiance half of the FA6 bot gate
is written and wired but unverified end-to-end over the wire, unlike the
fellowship half which is proven live.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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>
Register:
- AD-84 -- Swear button's missing "target is a player" enable-rule gate,
same class as AD-83's Recruit-button gap.
- AD-85 -- the unported StringInfo variable-substitution engine (AD-81's
same root cause) extended to the Allegiance page's numeric-only
followers/rank/experience-passed-up fields and its three local
confirmation dialogs (verbatim-or-bare-name, never invented).
- AD-86 -- ACE's deliberate zeroing of seven AllegianceProfile/
AllegianceData fields (officers, officer titles, MOTD, MOTD-set-by,
name-last-set-time, lock, approved vassal, timeOnline, allegianceAge),
dropped past acdream's own parse layer to match retail's own
gmAllegianceUI, which has no widget for any of them either.
- AD-82 addendum: the vassal-row click-target-only selection shares
point (3)'s limitation, but NOT the invented leader/selection tints
(point 1/2) or the Fellowship-only world-selection sync (point 4) --
Allegiance's list-selection message has no SetSelectedObject call.
Gate script: new docs/research/2026-08-12-campaign-fa-test-script.md
SFA5 section, mirroring SFA4's structure -- the CF-1 subscription steps
(including the reconnect-while-closed MF-3-REOPEN analogue), the SF-7
per-relationship monarch/patron steps, vassal-list steps, swear/break/
kick with their confirmations, the ACE-zeroed-field honesty note, and
full "what to report"/"explicitly not in scope" lists.
Plan ledger: FA5 row filled in against 7ed79eaf with per-item summary,
directly-measured totals (13,296/4/0, +11 net from FA4's 13,285/4/0),
and the two primary-source resolutions this slice needed beyond the
research docs (the self-rank field's live buffed-quality source, and
"your follower count" == _total_vassals, confirmed by a fresh targeted
decompile of UpdatePlayerData rather than inferred).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Register (docs/architecture/retail-divergence-register.md):
- AD-78: the Character-tab dimmed count had drifted stale through two
campaigns (still read "35" after FA4 shipped 31; now 34 after the fix
round's three reversions). Addendum explains the full D6/SF-8 chain.
Blast review's own SHOULD-FIX 1.
- AD-82 (new): the invented leader-tint/selection-tint colors, the
name-text-only row click target, and the page-local (not generic
UiTemplateListBox) world->panel selection sync -- MUST-FIX 4's
disposition plus two items MUST-FIX 5 named as owed rows.
- AD-83 (new): the Recruit button's missing "target is a player" gate,
previously an inline comment, not a register row -- MUST-FIX 5's third
item. Section header bumped 61 -> 63 active rows.
Gate script (docs/research/2026-08-12-campaign-fa-test-script.md):
- SF-7: fixed step 3's self-contradiction ("only Quit" then "Disband and
Open should ALSO be enabled").
- MUST-FIX 3: new reconnect step after the existing close/reopen step.
- MUST-FIX 4: new world-selection step under the recruit/dismiss/quit
section.
- MUST-FIX 1: new HARD-check step for the 6/8-fellow 44%/34% truncation
(distinct from the existing SOFT 9-member ACE-divergence note).
- MUST-FIX 2 correction: the old invite steps tested whether acdream's
CLIENT gates the dialog on the option bits -- a mechanism that never
existed in retail and no longer exists in acdream. Rewritten to test
the corrected behavior (the dialog always shows regardless of the
target's own checkbox state) and to explain what ACE-side filtering
would look like if the local server implements it, so a tester doesn't
misattribute ACE's behavior to a client bug.
- Renumbered steps 9-22 to 9-25 to fit the two new steps; updated the
"what to report" section's step cross-references and rewrote its
invite/dimming bullets to match the corrected mechanism.
Plan (docs/plans/2026-08-11-fellowship-allegiance-campaign.md):
- D7 addendum: SF-8's further correction (FellowshipShareLoot reverts
too; only FellowshipShareXP survives as genuinely live).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Register: AD-80 files the D5 XP-share display divergence between
retail's byte-decoded table (acdream renders it verbatim) and the
currently-targeted ACE server's slightly different actual grant (.3 vs
.3111111 at 9 fellows, no 10-fellow row, wrong out-of-range default) --
an ACE-vs-retail gap, not an acdream-vs-retail one, filed because it is
directly user-visible through this panel. AD-81 files the two unported
retail text-composition primitives the fellowship page's mechanism
needs (StringInfo variable substitution, ACCharGenData::FormatName) and
what acdream renders instead (plain numeric composites, the raw typed
name). AD-78's derivation table gains its D7 addendum: 4 of the 35
store-only rows (IgnoreFellowshipRequests/FellowshipAutoAcceptRequests/
FellowshipShareXP/FellowshipShareLoot) moved to the Live bullet with
their new consumers named.
Gate script: new §FA4 section covering create (name + shareXP), the
open/close caption swap, button-enable rules, and the D5 display -- all
solo-testable -- plus roster/recruit/dismiss/leader-handoff/invite-
dialog steps marked [TWO-CLIENT] with an honest note that they defer to
FA6's bot-vs-ACE gate if a second account isn't available for this
connected gate. Corrects FA3's now-stale "these six buttons/four
checkboxes are INERT" claims in steps 11-12 to point at the new
section instead of leaving a wrong claim in place.
Ledger: FA4 row CODE-COMPLETE with both commit SHAs, the reconciled
13,238->13,272 (+34) test-count arithmetic, the live-DAT verification
summary (ACDREAM_PROBE_LIVE_MOUNT=1 against real installed DATs,
including the structural finding that retail's own frame-visibility
swap already gates the Create-flow controls away from the roster view
with no extra code needed), and the four scoped
deferrals/simplifications this slice made (the StringInfo/FormatName
gap, the proportional-share omission, the Recruit button's
superset-of-retail enable rule).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Campaign FA slice FA3: retail's four-tab social panel (LayoutDesc
0x2100006E slot 0x1000018F, RetailPanelCatalog id 12), built on the OP3
OptionsPanelController recipe -- Type-8 tab host, ActivateTabBehavior,
per-page scoped controllers.
- SocialPanelController mounts the tab host and wires the close button;
F3 (ToggleAllegiancePanel) and F4 (ToggleFellowshipPanel) open the
panel and switch to their own tab, sharing the same gmPanelUI
one-active-panel exclusivity every sibling main panel already has.
- The live-DAT tab table CORRECTS the coordinator addendum's x-order
guess: button 0x1000028C ("Allegiance") pairs with page 0x10000291 and
is the authored DEFAULT entry, not Friends -- each button's own page
id and its own P0x57 (matching the F3/F4 ActionMap ids on the
Allegiance/Fellowship pages specifically) both corroborate the real
pairing. See SocialPanelController's class doc for the full table.
- SocialFellowshipPageController swaps the two authored empty/full
frames (0x1000026B/0x10000275) on RuntimeFellowshipState's
IsInFellowship -- both frames' full containment (name box, create
button, checkboxes vs. roster list, six buttons) was confirmed by the
live-mount probe, so a single Visible toggle per frame is the whole
swap (closes lane-A unknown U6).
- SocialAllegiancePageController hides the monarch/patron blocks and
blanks their name text to a literal space when
RuntimeAllegianceState.Snapshot.HasProfile is false, using SCOPED
FindDescendant lookups (the panel authors 0x10000492 twice, once per
block).
- SocialFriendsPageController/SocialSquelchPageController bind their
ListBoxes read-only to RuntimeCommunicationState's existing J4.1
Friends/Squelch owners (names only), rebuilding on revision change.
Their action buttons are honest INERT (D1) -- register row AD-79.
- UiTemplateListBox gains Flush() (lane A/D's "Gap found" prerequisite)
so a poll-and-rebuild list can shrink between refreshes.
- RetailPanelCatalog.SocialPanel = 12, byte-verified from the live slot's
own P0x10000029; listed in Mounted only (no toolbar button -- lane A
§6.1: the open path is keyboard-only).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
TS-81 filed: 0x027A AllegianceLoginNotification's retail two-line chat
text is not emitted -- the candidate strings resolve through Binary-
Ninja-mislabeled symbols at gmAllegianceUI::RecvNotice_AllegianceLogin
(0x00492220) that need a DAT string-table lookup, not a guess.
TS-80 partially narrowed: the fellowship-create shareXp wire mechanism
now exists end-to-end (IRuntimeFellowshipCommands.Create takes and
sends it), but no caller reads the option bit yet -- that's FA4's
create-dialog scope.
Updates the campaign plan's FA2 ledger row: code-complete, full test
totals (13,158/4/0 -> 13,201/4/0, +43 exact), the seam-doc dispatcher
correction, and the entity-table-borrow recommendation that wasn't
needed (the wire's own FellowMember record already carries full vitals
inline).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Applies the documentation-only MUST-FIX items from the blast review
(docs/research/2026-08-12-fa1-review-blast.md), plus mechanism SF-4:
Blast MF-1 / mechanism SF-4: register row AP-90
(retail-divergence-register.md) cited the deleted AllegianceTree class as
its evidence. Re-pointed to ClientCommandResponses.AllegianceProfileLookups
and the fellowship parsers FA1 added -- the deviation itself (radar
relationship state undelivered at runtime) is unchanged and NOT retired,
since FA2 hasn't wired a live owner yet.
Blast MF-2: corrected four falsified statements in the lane D research doc
(fa-acdream-seams.md), each marked with a dated, clearly-struck FA1
fix-round addendum rather than silently rewritten (it is a committed
research record):
- :791 "wrapping existing AllegianceTree" -- class deleted; re-pointed to
AllegianceProfileLookups.
- :666/:672 `commands.Fellowship.SetOpen -> BuildFellowshipUpdate` -- that
builder no longer exists; its renamed successor is panel visibility,
not openness, and using it here would re-introduce the exact semantic
bug FA1 fixed. Re-pointed to BuildFellowshipChangeOpenness (0x0291).
- :429/:668 `BuildFellowshipCreate(seq, name, openness, shareXp)` -- the
builder is now 3-arg; there is no wire openness field.
- :854 open question 8 (trailing-pad rule) -- ANSWERED by FA1 (VC-3),
closed with the answer instead of left open for re-derivation.
Blast MF-3: plan decision D9 and the FA1 slice-map row both asserted "the 8
missing fellowship WeenieError strings are added in FA1" -- FA1 shipped the
opposite, verified finding (no retail display text exists for any of the
8 ids). Both struck and annotated with the actual outcome.
Blast MF-4: reconciled the ledger's internally-inconsistent test-total row.
Direct measurement at the pre-fix-round tip (bc693728, stashed/restored
during this session to isolate it) confirms 13,149 passed / 4 skipped / 0
failed (13,153 total) -- the ledger's own prior number was actually
correct; the "baseline 13,103" and "net +50" framing next to it did not
reconcile with each other or with the diff-verified delta (+58 added / -9
deleted = net +49, one test of drift attributed to a different baseline
commit, not a further miscount). Also records this session's own +9 tests
and the blast SF-1 live-surface note (FA1 changed observable @allegiance
info output, not a purely-unwired slice).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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>
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>
Gate-3 screenshot review (user): 'the chat tab looks like it is missing
per window config' — Chat Window 1's header rendered over a void at the
DEFAULT scroll offset because its 260px self-sized filter block
straddled the viewport's bottom edge and UiScrollablePanel hid
straddling rows WHOLE (AP-201's predicted symptom, now user-observed at
scroll position zero, upgrading it from polish to blocking).
By fix time the UI renderer HAD everything needed: UiRenderContext's
clip stack (PushClip/PopClip with rect intersection + per-draw quad
clipping) and UiElement's ClipsChildren hook, already honored by both
the generic draw walk and hit-testing. The fix is therefore exactly the
shape the filing asked for, in the panel itself:
- ClipsChildren => true: children draw and hit-test clipped to the
viewport rect.
- The layout cull keeps any INTERSECTING row Visible (was: fully-inside
only), with a half-pixel margin excluding zero-overlap edge rows;
fully-outside rows stay hidden as the cheap skip.
AP-201 retired in this commit (AP actives 142 -> 141); #371 closed; the
gate script's Chat-tab steps re-written to expect clean edge clipping
and to treat any whole-block vanish as a regression. Pinned by
StraddlingRow_StaysVisible_AndClipsInsteadOfVanishing (the exact gate-3
geometry: header + 260px straddler in a 430px viewport) and
ViewportClipsChildDrawingAndHitTesting (the clipped slice is not
clickable).
Full Release suite: 13,089 passed / 4 skipped / 0 failed.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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>
Closes the OP9 combined review's findings (docs/research/2026-08-11-op9-review.md,
APPROVE-WITH-FIXES):
- MUST-FIX 1: SaveAudio -> ApplyAudio (OP6's Config-tab live-apply) lost
its ONLY assertion when the retired SettingsVM save-order test was
deleted. Restored directly on the now-public seam:
SaveAudioPersistsThenPushesLiveApplyAudioWithTheSavedSnapshot pins
persist-then-push order + the pushed snapshot;
SaveAudioSkipsTheLivePushWhenPersistenceFails pins the failure ordering
(a failed persist pushes nothing and commits nothing). Also closes
SF-5: the OP6 effective-volume comment's 'target-audio assertion above'
reference is real again and now names the restored test.
- SF-3: dead residues deleted — RuntimeSettingsController's private
SaveCharacter (zero callers post-371197a3), ISettingsStorage.SaveCharacter
+ its JsonRuntimeSettingsStorage/FakeStorage implementations (the deleted
private method was the only caller), and IngressShutdownRoots.Settings
(zero readers since the view-model shutdown stage died). SettingsStore's
PUBLIC SaveCharacter stays: it is the tested storage-API seam, and
per-toon entries in existing settings.json files still load through the
live LoadCharacter path.
- SF-2: code-structure.md's presentation-seam list no longer routes the
settings preview through 'optional SettingsVM'.
- NIT 6: AP-196's retirement note now attributes LockUI (/lockui +
PlayerDescription SetUiLocked convergence) and UseMouseTurning
(Gameplay-tab macro + Config-tab row) to their real channels instead of
folding all 13 members into the Character tab.
Full Release suite: 13,077 passed / 4 skipped / 0 failed (13,075 + the two
restored tests). One unnamed App-assembly failure appeared on the first
post-fix full run and did not reproduce on the isolated assembly rerun nor
a second full run — consistent with the known #250-class parallel-load
flake, recorded here for honesty rather than silently rerun.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Campaign OP slice OP9 code retirement (docs/plans/2026-08-10-options-panel-campaign.md
§OP9). The retail four-tab Options panel (OP1-OP8) is now acdream's one
in-client settings surface (D1) — this commit deletes the pre-retail-UI
surfaces it fully superseded. Pure retirement: no behavior change to
anything live, verified by dispositioning every one of the 15 src files
and 8 test files that referenced the deleted types before touching any
of them.
1. SettingsVM + SettingsPanel (the old F11 IPanel surface, unrendered
since Campaign V slice V11's ImGui deletion) deleted outright, plus
their two dedicated test files. IPanel/IPanelRenderer stay — ChatPanel/
DebugPanel/VitalsPanel still implement IPanel, so the contract does NOT
become unreferenced.
2. RuntimeSettingsController's SettingsVM binding seam deleted:
CreateViewModel, CreateViewModelBinding, RuntimeSettingsViewModelBinding,
the _viewModel field, UnbindViewModel, and every _viewModel? notification
call (ToggleFrameRate, LoadCharacterContext, RestoreDefaultCharacterContext,
SyncChatFromServerOptions). CreateViewModelBinding had zero production
callers (test-only). HasDraftPreview/DisplayPreview/AudioPreview stay on
IRuntimeSettingsPreviewSource (WorldRenderFrameBuilder and
SettingsParticleRangeSource still consume it) but now trivially mirror
the committed Display/Audio snapshot — HasDraftPreview was already
always false in production. GameWindowLifetime's matching
"settings view model" shutdown stage is deleted.
3. IDevToolsGameplayCommands + DevToolsGameplayCommands deleted from
GameplayInputCommandController.cs. All three members were dead:
ToggleSettingsPanel() had ZERO dispatch sites (ToggleOptionsPanel always
routed to _retained, never to _devTools); ToggleDebugPanel()/
FocusChatInput() had dispatch sites (F1/Ctrl+F1, Tab) but empty no-op
bodies (their ImGui DebugPanel/ChatPanel targets were already gone).
The two live dispatch sites are kept as inert `return true;` cases
(still consuming the key, matching the prior no-op's "handled"
contract) instead of falling through to a lower-priority scope.
SessionPlayerComposition.cs's `new DevToolsGameplayCommands()`
construction is removed. No `ToggleSettingsPanel` InputAction exists
(only `ToggleOptionsPanel`, rebound at OP3) — nothing to remove there
or from KeyBindings.RetailDefaults()/keymap fixtures.
4. GameplaySettings deleted entirely (the type, SettingsStore's
LoadGameplay/SaveGameplay/BuildGameplayObject, RuntimeSettingsController's
Gameplay property/SetAcceptLootPermits). Verified all 13 remaining
members (ToggleRun, AdvancedCombatUI, ShowTooltips,
VividTargetingIndicator, SideBySideVitals, CoordinatesOnRadar,
SpellDuration, AllowGive, ShowHelm, ShowCloak, LockUI, UseMouseTurning,
AcceptLootPermits — the three combat ones already died at OP4/AP-196)
were ALREADY bound through CharacterOptionTable/
CharacterOptionsPageController's server-bit seam at OP4 before deleting
the client-local mirror — no (c)-case genuinely-client-local member
was found; disposition (b) covers 100% of the surface. SetUiLocked
rewritten to compare only against the last value actually pushed to
_runtimeTargets (MUST-FIX 4's guard), with no second store left to
read or write. LiveSessionRuntimeFactory's SetAcceptLootPermits binding
now sends the wire option only (the GameplaySettings write-behind call
removed as dead output). CharacterSettings/DisplaySettings/
AudioSettings/ChatSettings and their SettingsStore Load/Save surfaces
are UNTOUCHED per the campaign contract.
Per-file disposition (15 src + 8 test files that referenced
GameplaySettings before this commit):
- GameplaySettings.cs, SettingsVM.cs, SettingsPanel.cs: the types
themselves — deleted.
- SettingsStore.cs, RuntimeSettingsController.cs,
LiveSessionRuntimeFactory.cs: real usage — API deleted/rewritten.
- RetailUiRuntime.cs, InteractionRetainedUiComposition.cs,
SessionPlayerComposition.cs, CombatUiController.cs,
LiveCombatAttackOperations.cs, LivePresentationComposition.cs,
FrameRootComposition.cs, RuntimeCharacterState.cs,
CombatCameraTargetSource.cs: doc-comment-only or interface-name
substring matches (ICombatGameplaySettingsSource) — left as accurate
historical record, no forward reference to the deleted type.
- Tests: RuntimeSettingsControllerTests.cs and SettingsStoreTests.cs
rewritten (Gameplay-specific tests deleted; SaveDisplay/SaveAudio/
SaveChat tests re-targeted off the now-public methods instead of the
retired SettingsVM draft/Save() indirection); GameplaySettingsTests.cs/
SettingsVMTests.cs/SettingsPanelTests.cs deleted; the remaining three
(CharacterOptionCombatSettingsSourceTests.cs,
CombatCameraTargetSourceTests.cs, LiveCombatAttackOperationsTests.cs)
were comment/interface-name-only, untouched.
5. Register: AP-196 (OP4's partial GameplaySettings retirement, which left
five fields as write-behind mirrors) is fully retired now that the
record is gone outright — marked ~~AP-196~~ RETIRED with its retirement
note, active-row count 143 -> 142. No other row cited the deleted types
directly (AP-194/AP-193 cite CharacterOptionTable.cs, not
GameplaySettings.cs).
6. Settings.json migration honesty: SettingsStore no longer reads or
writes the "gameplay" top-level key, so an existing file carrying one
from a pre-OP9 build is neither parsed nor dropped — the existing
SaveSection raw-JSON-text preservation mechanism (unknown top-level
keys survive every subsequent save) carries it forward untouched.
Two new targeted tests
(LeftoverGameplaySection_FromAnOlderSettingsJson_DoesNotBreakOtherLoads,
LeftoverGameplaySection_SurvivesAnUnrelatedSave) pin this.
InputAction.ToggleOptionsPanel's stale doc comment (still describing the
retired ImGui SettingsPanel) and a handful of other dangling doc
references (DisplaySettings.cs, ChatOpacityLink.cs,
SettingsDevToolsComposition.cs, InputDispatcherCaptureTests.cs) are
reworded to point at the current retail Options panel / OP8
KeyboardConfigController.
Build: dotnet build -c Release green, 0 errors. Tests: dotnet test -c
Release --no-build — 13,075 passed / 4 skipped / 0 failed (13,079 total),
down from the stated baseline of 13,155 passed / 4 skipped / 0 failed
(13,159 total) — the -80 delta is exactly the deleted SettingsVM/
SettingsPanel/GameplaySettings test surface (three whole files plus the
Gameplay-specific cases trimmed from RuntimeSettingsControllerTests.cs/
SettingsStoreTests.cs), with zero regressions elsewhere.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
Mounts retail's Options panel (LayoutDesc 0x2100002B resolved through host
0x2100006E slot 0x1000018D, gmPanelUI key 10) via the same catalog-import
pattern CharacterController already validates, registered through
RetailPanelUiController so it shares retail's "one active gmPanelUI child"
mutual exclusion with every other sibling panel for free. F11 and the
toolbar's options button (0x1000019B, already authoring panel id 10) both
now open it; the close button fires the same ToggleOptionsPanel action.
OptionPageModel (OptionPage/BoolOptionRow) ports retail's exact
Apply/Reset/Defaults/visibility semantics from
UIOption_Checkbox/PlayerOptionPage — LED clicks apply live immediately,
Apply commits every row unconditionally + flushes the batched blob, Reset
reverts only Changed rows, Defaults restores without committing, and
tab-switch/window-hide revert uncommitted edits. Wired for all four tabs;
this slice registers real rows on none of them (Gameplay authentically has
none — a pure button list). UiTabPanel gains an ActivePageChanged event so
the page model can hook every tab transition, including the initial
default-tab activation.
The seven Gameplay-tab buttons: Exit Game reuses the existing graceful
window-close path; Exit to Character Selection gets retail's confirmation
dialog and byte-verified mid-air refusal but still behaves as Exit Game
(AD-74 — no pre-world character-select flow exists); Configure Keyboard
and In-Game Help Files are inert this slice (AD-76 for Help — the
plugin retail depends on doesn't exist); Urgent Assistance/Report Abuse
short-circuit to their own byte-verified failure text through the
interface-text seam instead of ShellExecute against a dead URL (AD-75);
Use Mouse Turning Settings runs the pure MouseTurningSettingsMacro port,
persisting five new CameraTurningSettings preferences and sending
PlayerOption.UseMouseTurning — TS-74 records that acdream has no
persistent mouse-turning camera mode for the bit to drive yet.
Full Release suite: 12,918 passed / 4 skipped / 0 failed (baseline
12,871/4/0 — only new tests added).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Closes the mechanism lens's REOPEN (one MUST-FIX) and both lenses' small
residuals on the OP2 rework (b236a442); the blast lens re-review was
CLOSED outright. Fable-direct per the two-failure escalation rule.
- AP-195 filed: UIOption_CheckboxBitfield64 ports HALF of Refresh
@0x004859C0 — the ANY-set checkbox predicate is exact, but the ALL-set
LED media swap (P0x10000082=0x06004D17 / P0x10000083=0x06004D19) and
the ListBox self-sizing tail (ResizeTo/CalculatePaperSize — the block
IS a UIElement_ListBox in retail) are unported, and the block's row
stacking is a second divergent implementation beside UiTemplateListBox.
All due at OP5 before the Chat tab's connected gate; the IsSet doc
comment now names both halves instead of quoting only the ported one.
- Row tooltips: UiButton gains settable TooltipText surfaced through the
shared GetTooltipText hover pipeline (UiCatalogSlot's pattern);
UiCheckboxBitfield64.AddChild applies the row tooltip retail stamps in
CreateChildren @0x00485DF0, and documents that the 0x10000084 row-index
attribute stamp is deliberately replaced by the typed mask closure.
- AD-73 addendum: UiTemplateListBox.ConsumesDatChildren=true is inert
only while no authored Type-5 element carries children — that premise
is now conformance-PINNED across all 32 fixtures (a future DAT
regeneration surfacing an authored child fails the build instead of
silently dropping it).
- Plan doc: OP2's contract names UiTabPanel.cs (retail UIElement_Panel),
not the fictional-class-named UiTabControl.cs; ledger records OP1 and
OP2 both CLOSED.
Full Release suite: 12,871 passed / 4 skipped / 0 failed.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
OP2 (df9c7a35) was double-REJECTed: an unconditional Type-8/Type-5 factory
mapping silently re-classed 15 elements across 7 shipped panels (vendor
backdrop lost its fill, character/spellbook roots stopped passing clicks
through, combat gained a phantom import-time tab takeover, ten ListBoxes
gained a spurious hit-testable viewport) because the stale 27 pre-existing
fixtures never exercised the new fields — and the mechanism itself cited a
nonexistent "UIElement_TabControl" class, inverted UiCheckboxBitfield64's
checked-state predicate, and synthesized fake per-row geometry instead of
using the widget's own authored template.
Shape change: UiTabPanel (renamed from UiTabControl) and UiTemplateListBox
now derive from UiDatElement (unsealed) and stay DORMANT by default — an
imported Type-8/Type-5 element gets authored-media drawing, ClickThrough
generic-decoration default, and IUiDatStateful propagation identical to the
pre-OP2 UiDatElement fallback, with zero import-time side effects. The
factory's Type-8/Type-5 arms are unconditional again (no more guard whose
premise the blast-radius sweep proved false), because dormancy makes an
unactivated instance behaviorally indistinguishable from the old fallback.
UiTabPanel.ActivateTabBehavior() and UiTemplateListBox's lazy viewport
creation are the explicit, controller-driven opt-ins Campaign OP slice OP3+
will call; today nothing does, so the four pre-existing shipped Type-8
hosts (character/spellbook/vendor/combat) and ten pre-existing Type-5
ListBoxes keep their pre-OP2 behavior exactly. Filed AD-73 for this
dormant-vs-retail's-unconditional-activation adaptation.
Mechanism fixes (docs/research/2026-08-11-op2-review-mechanism.md):
- UiTabPanel cites UIElement_Panel (Type 8 is UIElement_Panel; no
UIElement_TabControl exists in the PDB), resolves buttons/pages via a
GetChildRecursive-equivalent descendant search (not direct-children-only),
performs no switch when no entry authors 0x32 (deleted the _tabs[0]
fallback), and surfaces unresolved tab-table entries via UnresolvedEntries
+ a diagnostic line instead of a silent no-op.
- ElementReader.ReadTabTable skips entries missing 0x30/0x31, matching
retail's SetupTabPageHash @0x0046C2E0 entry filter.
- UiCheckboxBitfield64 now builds every row from its OWN authored template
(property 0x64 -> {0x2100002B, 0x10000521}) via AddItemFromTemplateList,
deleting the synthesized ElementInfo + invented RowHeight=14 — matching
retail's CreateChildren @0x00485DF0, which is itself a UIElement_ListBox
call. IsSet is now retail's ANY-bit-set predicate (Refresh @0x004859C0),
not all-bits-set. TS-72 retired: the click-toggle bit math is now fully
decomp-confirmed (SetBitsOnOrOff via ListenToElementMessage @0x00485AE0).
Regenerated all 32 UI fixtures against real DAT (ACDREAM_REGENERATE_UI_FIXTURES=1)
and committed them — 27 pre-existing fixtures now carry Outline/OutlineColor/
TabTable/TemplateList/ScrollbarElementId; the 5 Options fixtures were already
current. Updated EffectsUiControllerTests' now-correct UiTemplateListBox
class-identity assertion. Added: 6 built-widget behavior pins for all five
pre-existing Type-8 elements + a representative Type-5 element the dormancy
model protects (OP2ReworkBlastRadiusConformanceTests.cs); 5 reader-level
tests driving ReadTabTable/ReadTemplateList/the 0x72 reader from raw
property bags (ElementReaderTests.cs); a multi-bit-mask UiCheckboxBitfield64
test proving the any-bit predicate (the prior single-bit test couldn't
distinguish it from all-bits); an activation-idempotency test and a
before-activation click-is-inert test for UiTabPanel.
Full Release suite: 12,868 passed / 4 skipped / 0 failed (baseline 12,853/4/0
post-OP1-fixes; +15 net new tests, zero regressions).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Closes the two mechanism-lens and blast-lens dual reviews of Campaign OP
slice OP1 (86c0a7e0): docs/research/2026-08-10-op1-review-mechanism.md and
docs/research/2026-08-10-op1-review-blast.md.
MUST-FIX M1 (blast): RuntimeCharacterOptionsState gains a HasServerSeed
latch, set by Replace (the PlayerDescription seed) and cleared by
ResetSession. TryFlush/TryFlushIfAutoSaveDue now refuse before the seed
arrives — closing the window where a bot (or, after this commit, the
timer/logout triggers) could flush client-default option words over a
character's real server-side options before any PlayerDescription ever
landed.
MUST-FIX 1 (mechanism): the 480 s auto-save timer and the pre-logoff
flush are now wired into production, closing TS-71 (retired). Both ride
LiveSessionController's own tick/stop transaction via two new hooks
(ConfigureAutoSaveTick/ConfigurePreLogoffFlush), wired once by
GameRuntime's constructor — a Runtime-internal change requiring zero
host edits, exactly as the review identified. The flush body talks to
WorldSession directly rather than through App's LiveSessionCommandRouter,
which is what keeps this off the S2 lock-order hazard (below). Filed
TS-73 for the two OnChanged side-effect cases (weather/day/combat-
target/fog) TrySetOption still doesn't model — pre-anchored to OP4's
Group B consumer binds.
SHOULD-FIX S2 (blast, prerequisite for MUST-FIX 1): TryFlush/
TryFlushIfAutoSaveDue no longer invoke the flush callback while holding
_dirtyGate — the decision is made and cleared under the lock, but the
callback itself runs outside it, closing the lock-inversion hazard the
natural timer wiring would have hit (Runtime tick's _dirtyGate-then-
_gate vs the router's _gate-then-_dirtyGate).
SHOULD-FIX MF-2 (mechanism): TrySetOption now ports the two
PlayerModule-state-mutating cases of CPlayerModule::OnChanged's local
side-effect switch — turning ON IgnoreFellowshipRequests or
FellowshipAutoAcceptRequests clears the other through a real recursive
TrySetOption call, reproducing retail's second 0x0005 (the clear's send
reaches the wire before the primary option's own send, matching the
nested-call order in the decomp). The signature widened from
Action sendAutoSave to Action<uint,bool> so the recursion can send a
different (id, value) than the caller's own; every production call site
now passes WorldSession.SendSetSingleCharacterOption directly.
SHOULD-FIX MF-3 (mechanism): a hand-transcribed 53-row (id, isOptions1,
mask) theory in CharacterOptionTableTests, independently re-derived from
acclient.h's PlayerOption/CharacterOption/CharacterOptions2 enums rather
than copied from CharacterOptionTable.cs — closes the one column with no
id-by-id pin. Also added the pairwise-distinctness check blast NOTE N7
named.
SHOULD-FIX S1 (blast): LiveSessionCommandRouterTests' CH3/CH4 regression
test now drives the REAL TrySetOption binding instead of a hand-rolled
SetOptionBit substitute that had silently drifted from production after
OP1.
SHOULD-FIX S3 (blast): RuntimeCharacterOwnershipSnapshot gains
OptionsAreClean (!Options.IsDirty), included in IsConverged — a module
whose two words happen to cycle back to their default bit pattern while
still dirty is now caught by the combined ownership ledger, not just by
OptionsAreDefaults.
SHOULD-FIX S4 (blast): SaveOptions no longer encodes "did it actually
flush" as PrimaryObjectId 1u/0u (which read as object guid 0x00000001 in
the K2 event stream). Both host adapters now report the identical shape
(Accepted, objectId 0) — the graphical host never could report this
anyway (LiveCommandBus.Publish has no return channel).
SHOULD-FIX S5 (blast): Replace (the server-seed arrival) now also clears
IsDirty/FirstDirtiedAt — a wholesale re-seed supersedes any pending
batched-but-unflushed local intent (retail's own PlayerModule has no
partial-merge path either), documented at the member.
SHOULD-FIX S6 (blast): a cross-check theory asserting CharacterOptionTable's
masks equal PlayerDescriptionParser.CharacterOptions1/2's independently
(the write path vs the read path TurbineChatMembershipGate/
RuntimeSettingsController consume) — guards the exact CH3 failure class.
Also fixed a real allocation regression found while landing MUST-FIX 1:
the naive per-tick flush closure would have allocated on EVERY
LiveSessionController.Tick() call regardless of dirty state, which broke
the K4 headless 30-session resource-envelope gate. GameRuntime.
FlushCharacterOptions now pre-checks Options.IsDirty (itself retail-
faithful — CPlayerModule::UseTime opens with the identical m_bDirty byte
compare) before allocating the flush closure, so the allocation only
happens on the rare tick that might actually flush.
Dispositions on findings not changed this round:
- Mechanism NOTE 6 / not independently re-flagged: a re-entrant MarkDirty
from inside a flush callback can still be erased by the trailing
"_isDirty = false" — pre-existing, unchanged by the S2 lock restructure
(same outcome whether the callback runs inside or outside the lock),
not reachable from any current caller, not a one-liner to close
correctly (needs a per-dirty-period generation token). Left as documented
in the review; worth closing before the Options panel ever flushes from
inside a change handler.
- Mechanism NOTE 9, blast N2/N3/N4/N5/N6/N8: informational or require
touching files this round doesn't otherwise edit (SocialActions.cs,
CharacterOptionsBlobSource.cs, GameRuntimeContractTests.cs) — left per
the "one-liner in a file already being edited" instruction.
Register: TS-71 retired (both remaining SetCharacterOptions flush
triggers now production-wired); TS-73 filed (the two unmodeled OnChanged
presentation-binding cases, pre-anchored to OP4).
Quality bar: Release build green; full solution suite 12,853 passed / 4
skipped / 0 failed (baseline 12,770/4/0 post-OP2 — 83 new tests added,
zero regressions).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Ships the two new widget primitives the retail Options panel needs plus the
four remaining UIOption_* factory mappings, so every tab page (OP3-OP6) has
somewhere to mount.
- ElementReader/ElementInfo gain three new dat-property readers, following
the existing effective-state-resolution pattern (never a per-state
first-wins scan, per the round-5 N1 lesson): the Type-8 tab table
(property 0x2E -> TabTable), a ListBox's row-template list (property
0x64 -> TemplateList), and scrollbar linkage (property 0x72 ->
ScrollbarElementId). LayoutImporter gains one hook
(IUiChildrenAttachedListener) so a widget can resolve cross-references
its own dat properties name by id once its subtree actually exists.
- UiTabControl (Type 8): switches exactly one page-slot child visible,
syncs each tab button's Open/Closed state via the existing
RetailTabBinding helper, and honors the authored default tab on mount.
- UiTemplateListBox (Type 5 with an authored template list): wraps a
UiScrollablePanel viewport (sealed, so composition not inheritance) and
ports AddItemFromTemplateList(index) — the resolver seam a page
controller wires with real DAT access via the SAME
LayoutImporter.ImportInfos(dats, layoutId, elementId) overload
RetailDialogFactory already uses for its catalog LayoutDesc.
- DatWidgetFactory maps the four remaining UIOption_* widgets, each
verified against the regenerated options_2100002B.json fixture before
writing any code: 0x10000037 (Slider) is structurally an ordinary
horizontal UIElement_Scrollbar, so it reuses BuildScrollbar directly;
0x10000038 (Menu) is structurally identical to the vendor category
dropdown UiMenu already models, so it reuses `new UiMenu()` like the
Type-6 case; 0x10000036 (CheckboxSlider) composes an existing
UIOption_Checkbox child + UIOption_Slider child via the new
UiOptionToggleSlider wrapper; 0x10000044 (CheckboxBitfield64) authors
zero children in the dat (every row is added at runtime via retail's own
AddChild(lowMask, highMask, label, tooltip) call shape), so it's a new
UiCheckboxBitfield64 composing UiButton per row. No new drawing code
anywhere in this set.
- Five new committed fixtures (options_2100002B/2100002A/21000028/
2100005C/21000029) plus 25 new conformance tests pinning the tab table
(4 entries, Gameplay default), all three template arrays, scrollbar
linkage, every new widget-type mapping, and a UiTabControl behavioral
test (switch -> exactly one page visible, click-through the tab
button). The Character ListBox's authored 6-header/49-toggle shape
(lane B section counts) is proven reachable end-to-end through
AddItemFromTemplateList against the committed fixture.
- Regenerating fixtures also touched 27 PRE-EXISTING, unrelated fixtures
(an Outline/OutlineColor field pair added by an earlier commit,
bcc34ee3, that predates when those fixtures were last regenerated).
Per the slice contract, that drift was NOT committed — reverted back to
HEAD, only the five new Options-panel fixtures are new files here.
- Filed TS-72: UiCheckboxBitfield64's click-toggle bit math (AND/OR
set/clear semantics) is a documented approximation — the decompiled
excerpt this campaign pulled covers UIOption_CheckboxBitfield64::Apply's
WRITE side, not its own click-handler's bit math. Flagged for OP5 (the
Chat tab controller, the first consumer that reaches the wire) to
verify against the real decomp before any live transaction depends on
it; nothing user-reachable can observe this yet.
Full Release suite: 12,770 passed / 4 skipped / 0 failed (was 12,745/4/0
post-OP1 — 25 net new tests, zero regressions).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The retail Options panel (Campaign OP) needs a Runtime-owned option map
covering all 53 PlayerOption ids and the real batched SetCharacterOptions
(0x01A1) blob before any UI can be built on top of it. Today's surface only
modeled 6 ListenTo*Chat ids and the 0x01A1 builder was a malformed 16-byte
stub (deleted at Campaign CH slice CH3, docs/research/2026-08-09-chat-side-
channels-vs-ace.md).
- CharacterOptionTable.cs: the ONE typed table, PlayerOption id (0x00..0x34)
-> (Options1/Options2 word, mask, IsAutoSave, ClientDefault), transcribed
from acclient.h's verbatim CharacterOption/CharacterOptions2/PlayerOption
enums and byte-verified against IsAutoSaveOption @0x0059A600 (the 21-id
auto-save table) and GetDefaultOptionValue @0x005D2A30 (the Defaults-
button table). Reconstructing CharacterOptions1/2 defaults from the
ClientDefault column independently reproduces 0x50C4A54A / 0x00008700,
cross-confirming the id-mask mapping. CharacterOptionId (SocialActions.cs)
widened from 6 to all 53 ids to match.
- RuntimeCharacterOptionsState: SetOptionBit now resolves through the full
table (was a 6-case switch). New TrySetOption is the ONE shared local-
write-then-send/dirty seam — mirrors CPlayerModule::OnChanged exactly:
write the bit locally first, then either send 0x0005 immediately (auto-
save ids) or MarkDirty for the batched blob, no-op on an unchanged value
(retail's own early-return) or an unmodeled id. New dirty model (IsDirty/
FirstDirtiedAt/MarkDirty/TryFlush/TryFlushIfAutoSaveDue) uses an injected
TimeProvider so it's fully unit-testable without a live clock.
- Both IRuntimeCharacterCommands.SetSingleOption adapters (Direct + Current)
now route through TrySetOption instead of duplicating the write; this
fixes the headless local-write gap the OP1 research flagged (the direct
adapter previously sent the wire message without writing the bit first,
same class of bug CH4 fixed for the graphical host). Both also reject an
id outside the table instead of silently accepting it. LiveSessionRuntime
Factory's SendSingleCharacterOption closure now delegates to the same
seam instead of duplicating write-then-send inline.
- New IRuntimeCharacterCommands.SaveOptions(generation) — the explicit
blob-flush verb (retail's SaveToServer(force: 0)) — wired end-to-end in
both adapters, including a new SaveCharacterOptionsRuntimeCmd on the
graphical router.
- SocialActions.BuildSetCharacterOptions + WorldSession.SendSetCharacterOptions:
the real PlayerModule::Pack body per the wire research's field-by-field
layout — header always 0x460 OR'd with 0x001/0x008 when shortcuts/desired
comps are non-empty, favorite spells always 8 lists, never sets 0x100 or
0x200. Echoes last-parsed shortcuts/favorites/desired-comps/spellbook
filters (via new CharacterOptionsBlobSource) instead of zeroing them.
Conformance: a hand-computed golden byte vector (not generated by the
builder under test — the CH3 builder died of tests that pinned a wrong
shape and looked green) plus a round-trip through PlayerDescriptionParser.
Contract deviation: the 480 s auto-save timer and the flush-before-logout
trigger are implemented as fully-tested pure state-machine logic
(TryFlushIfAutoSaveDue) but are NOT wired into either host's live per-frame
loop or graceful-shutdown sequence in this slice — only the explicit
SaveOptions verb is production-wired. Wiring the timer touches App's
UpdateFrameOrchestrator graph and Headless's tick loop (outside this
slice's Runtime/wire-layer scope); wiring logout risks the already-fragile
graceful-shutdown sequence CLAUDE.md flags. Filed as TS-71 per the plan's
own escape valve ("target: not deferred" with a register row if deferred).
Also filed: AP-193 (the 0x34 HearPKDeathMessages id/mask is ACE-sourced,
unverifiable against the 2013 binary) and AP-194 (GetDefaultOptionValue's
table disagrees with the constructor default for ConfirmVolatileRareUse/
ShowHelm/ShowCloak — retail's own quirk, reproduced not fixed).
Tests: table completeness x53, auto-save/client-default split pinned
id-by-id against the byte-verified tables, unknown/reserved-id rejection
(0x35/0x36 landmines), local-write-then-send on both adapters + the router,
the dirty/flush state machine, SaveOptions, and the wire golden vector +
PlayerDescriptionParser round-trip. Full Release suite: 12,745 passed / 4
skipped / 0 failed (baseline 12,611/4/0 — slice adds 134 passing tests,
zero regressions).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Collects the post-gate polish left uncommitted by the killed round-5 agent
(S1/S3 + review fixes N1/N3/N4) and completes the missing S2 half:
- S1: UiText multi-line transcript + colored-run label now submit EVERY
line/run's outline pass before ANY fill pass, matching retail's
UIElement_Text::DrawSelf @0x00467aa0 whole-block walk. DrawStringDatPass
is exposed for block-level batching; single lines keep DrawStringDat.
- S2 (completed this commit): authored outline 0x21/0x22 now reaches every
text-bearing widget — UiButton, UiDatElement, UiField, UiMeter, UiMenu,
UiCatalogSlot — seeded from the element's effective-default state exactly
like UiText (BuildButton lifts the label-bearing Text child's authored
value first, same chain as the label color). Per-STATE outline switching
(dialog/character/combat buttons author 0x21 in state 0x3 only) is NOT
ported — filed as register row AP-192 in this commit.
- S3: ChatWindowController reconciliation comment corrects the misread
indicator action ids 0x10000514-17 -> 0x10000114-17 and re-attributes the
id-coincidence to the pagination widget's m_prevButton/m_nextButton, not
gmFriendsUI; register + window-shell research doc corrected to match.
- N1: LayoutImporter's duplicate per-state any-state-first-wins 0x21 read is
deleted — ElementReader.ApplyCanonicalLegacyProjection's DirectState-then-
effective-default resolution is the single source (the duplicate would have
lit state-0x3-only outlines permanently once S2 widened consumption).
- N3: the outline pass tints with the outline color's OWN alpha, not the
fill's (retail tints m_curOutlineColor and m_curTextColor independently).
- N4: the outline-inflated glyph SOURCE rect is clamped to the atlas bounds
with matching dest shrink, porting CreateCharRectPair @0x00441480's edge
behavior — edge glyphs crop instead of sampling a neighbour's texels.
Full Release suite: 12,610 passed / 4 skipped / 0 failed.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Campaign CH round 4, user-gate items 1+2. Root cause: retail ships a
second (background) glyph atlas per font, dilated 2px on every side,
plus two border-pixel scalars (Font.NumHorizontalBorderPixels/
NumVerticalBorderPixels) that acdream's font reader never read — so
even the pre-existing outline parameter drew almost nothing once
enabled. Landed together (either half alone is a no-op or a
regression):
- UiDatFont carries BorderX/BorderY from the DAT font resource.
- UiRenderContext.DrawStringDat inflates the background blit's source
and destination rect by that margin and restructures into retail's
exact two-pass whole-string outline-then-fill model
(UIElement_Text::DrawSelf), plus the 8-neighbour +-1px fallback for
fonts with no background atlas. Corrects the stale "property 0xd"
comment to the real ids, 0x21 (Outline) / 0x22 (OutlineColor).
- LayoutDesc property 0x21/0x22 import (ElementInfo.Outline/
OutlineColor, LayoutImporter.ReadState, ElementReader.Merge/
ApplyCanonicalLegacyProjection, DatWidgetFactory.BuildText) so every
authored-outline element across the DAT set is correct at once.
- SpewBox: RetailFontId corrected from a round-3 heuristic
(0x40000025) to the actually-authored 0x40000001 (18px bold serif),
Outline=true set on the controller's UiText. Fill colour stays the
user-gate-round-1-pinned yellow — font atlases are alpha-only
(PFID_A8), so there is no baked shading that could explain the
screenshot's gold as anything other than the outline itself.
- Chat transcript: default fill now seeds from its authored
ARGB(255,204,204,204) instead of an unrelated color-table slot
(ChatTranscriptRenderer.BuildLines takes the transcript's own
DefaultColor as a parameter); the 34-entry LogTextType table is
untouched, and every existing CH1 conformance test stays green
unmodified.
Regenerated the committed chat_2100006f.json fixture from the real
installed DAT, confirming end to end (not by missing-field default)
that the transcript carries no outline.
Tests: font-reader border fields + inflation math pinned against the
real DAT font, two-pass draw ordering/tint/inflation via a new
TextRenderer.DebugSpriteSegmentVerts test seam, property 0x21/0x22
import at both the ElementReader.Merge and StateDesc-property layers,
SpewBox font/outline, and the chat default-shade seed with the color
table proven untouched.
Full Release suite: 12,610 passed / 4 skipped / 0 failed
(AcDream.slnx, complete solution).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>