The narrow re-review (96df892d) closed FA1 with no reopen and one
carry-forward: two further forward-looking seam-map rows (:128 owner
diagram, :214 state-parameter pattern) still cited the deleted
AllegianceTree as FA2 design guidance. Both now carry dated strike/
addendum notes pointing FA2 at the parsed profile records instead.
FA2 unblocked per the re-review's precondition.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Narrow re-review of the fix round (ed308087 code+tests, 511ba6e5 docs)
against the mechanism findings doc. Verdict CLOSED, no REOPEN.
All 2 MUST-FIX and 5 SHOULD-FIX verified in the actual diffs, each
re-derived rather than taken on the commit message's word:
- MF-1: zero-id rejection on BOTH the monarch and child paths, which is
also what makes treeParent == 0 provably fatal (knownIds can never
contain 0); three boundary tests.
- MF-2: the 0x001F builder re-verified against primary source --
CM_Allegiance::Event_UpdateRequest @0x006A7260 allocates 0x10, stores
0x1f at 006a72ba, writes the arg as a full u32 at 006a72cb. Both golden
vectors correct; all five new anchors resolve; the ACE claim
(GameActionAllegianceUpdateRequest.cs:12 reads and ignores the value)
is accurate.
- SF-1: monarch clear placed at retail's own position/guard; the fixture
relocation onto a vassal is not just correct but necessary, since the
clear would otherwise mask the legacy-compat fallback.
- SF-2/SF-3/SF-5 all closed; SF-5 resolved better than asked, renumbering
to the real AllegianceVersion enum values (verified against
acclient.h:2979-2994) and naming gate 5 as real-but-gating-nothing.
Spot-verified all six blast dispositions: AP-90 re-pointed without being
wrongly retired; four seam-map corrections applied as dated strikes (its
open-question-8 answer independently re-verified against PackString16L
and ACE's ReadString16L pad skip); D9 + slice row struck and annotated;
ledger arithmetic now closes (13,153 total sums correctly, -4 skips =
13,149).
Suite claim corroborated: the fix diff adds exactly +9 [Fact]/[Theory]
and removes 0, and the post-fix Release binaries (stamped after
ed308087, so --no-build is legitimate here) measure AcDream.Core.Net.Tests
at 886/0/0 -- exactly the blast doc's 877 pre-fix anchor plus 9, with all
9 new tests in that project.
One carry-forward, NOT a reopen: blast MF-2's enumeration stopped at four
rows; lane D still names the deleted AllegianceTree at :128 and :214,
both forward-looking FA2 design guidance of the same danger class as the
:791 row that was corrected. Two more dated addenda close it; FA2 should
not start before that.
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>
Mechanism-faithfulness lens on 7be86f47/6bedbc47/5f9aa16f/4281750b.
Every wire layout re-derived independently from the retail decomp rather
than taken on the lane docs' word: Fellow::UnPack, Fellowship::UnPack,
PackableHashTable::UnPack's count/buckets split, DispatchUI_UpdateFellow's
guid-first read, Event_Create's single trailing u32 shareXP,
AllegianceHierarchy::UnPack's eleven gates + their non-monotonic wire
order, AllegianceHierarchy::Add, AllegianceProfile::UnPack, the 0x20
dispatch case. All six golden byte vectors re-computed field by field --
no encoding, padding, or endianness slip found.
Both premise-contradiction calls VERIFIED CORRECT from primary source:
the 8 fellowship WeenieError ids genuinely have no case label, no else-if
comparison, no decimal form and no default fallthrough in
HandleFailureEvent (D9's premise was wrong, the refusal to invent English
was right); and 0x0275 is client-authored, so the typed ConfirmationType
enum -- not a receive parser -- was the real gap, and D6's FA4/FA5 flows
are buildable on what landed.
MUST-FIX: (1) AllegianceHierarchy::Add's fourth rejection rule (_id == 0,
which also makes treeParent == 0 unconditionally fatal) is unmodeled;
(2) the 0x001F AllegianceUpdateRequest builder -- the allegiance twin of
the 0x00A6 this slice repaired, and lane C's #3 minimum-viable message --
is missing entirely.
SHOULD-FIX: monarch MayPassupExperience is not force-cleared;
ParseFellowshipDisband validates a body length retail never inspects;
D5's <<1 shape is unpinned at the 0x02C0 site; AP-90's register row still
cites the deleted AllegianceTree; the gate comment numbering stops at ten.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Blast-radius lens over Campaign FA slice FA1 (7be86f47, 6bedbc47,
5f9aa16f, 4281750b, ee1124ca). The wire work is right -- both repaired
builders re-derived against ACE's own handlers, all eleven allegiance
version gates against AllegianceHierarchy::UnPack @0x005B7520, and both
tree-assembly rules against Add @0x005B6E90. No handler-lane collision,
no double registration, no cross-host source touched.
The bookkeeping is not. MUST-FIX: register row AP-90 still cites the
deleted AllegianceTree; lane D's seam map -- FA2's own contract -- is
falsified in four places including a row that would make FA6 re-introduce
the exact openness/panel-visibility bug FA1 fixed; plan decision D9 still
says the 8 WeenieError strings were added when FA1 shipped the opposite
finding; and the ledger's test totals state three mutually exclusive
numbers (+46 implied, +50 stated, +49 measured from the diffs).
SHOULD-FIX: the "UNWIRED" framing is untrue of ParseAllegianceInfoResponse
(live behind @allegiance info -- vassal print order now reverses and a
malformed tree now silences the command outright; both retail-faithful,
neither pinned by an order-sensitive test); the id != 0 discard rule is
missing; retail's monarch MayPassupExperience zeroing is missing; one new
doc comment cites a nonexistent test class; and the confirmation triple
now carries its discriminator two ways.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Four Opus research lanes for the Fellowship & Allegiance campaign:
panel structure, fellowship wire (two BN zero-folds broken by byte
decode: IsFull >= 9, the x87 XP-share table capping at 2.8x), allegiance
wire (27+5 messages binary-verified; tree assembly discard/reversal
rules; ACE zeroed-field caveats), and the acdream seams audit (H.2
scaffolding inventory, J-owner recommendation, AD-78 dimmed-row
inventory, bot-gate requirements).
Coordinator U2 closure (FaPanelSlotProbeTests, live DATs): Fellowship
and Allegiance are two of FOUR pages of ONE tabbed social panel — slot
0x1000018F, panel id 12, Type-8 host — alongside gmFriendsUI and
gmSquelchUI; lane A's separate-siblings mounting call is corrected in
its addendum, and the full 16-slot dump closes every unidentified
RetailPanelCatalog entry (Abuse/Book/LinkStatus/MiniGame/UA/Vitae/
Map+House/Journal).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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>
Root cause: PlayerOptionPage::AddSliderOption never sets a row's own
name-label text — the "Inactive Opacity"/"Active Opacity" caption comes
from a SEPARATE DAT-resident runtime catalog (DID 0x78000000, resolved
via the same two-level DBCache::GetDIDFromEnumStatic master-map/submap
lookup ChatOptionsDatDefaults already uses for enum 0x16/category 2,
here for enum 0x15/category 2) that nothing in the codebase ever
queried, so both slider rows rendered with no caption at all.
Fix: new ChatOptionsDatCaptions.TryRead resolves the DID-0x78000000
catalog's per-property name/tooltip entries (matched by the same
owning-property enum ChatOptionsDatDefaults already keys its defaults
by) and ChatOptionsPageController.BuildOpacitySliders stamps each
slider's own row caption/tooltip from it — falling back to no text
(never invented English) if resolution fails. Regressed by
ChatOptionsPageControllerTests.
Bind_WiresEachSlidersOwnRowCaption_FromTheResolvedDatCatalog and the
companion Bind_MissingCaption_RendersNoText_NeverInventsEnglish case,
plus a live-mount probe (OptionsPanelLiveMountProbeTests.
ProbeChatOpacityCaptions) confirming the production TryRead call
resolves "Inactive Opacity"/"Active Opacity" against the real DAT.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Root cause: RetailWindowOpacityController applied the Default/Active
opacity fade to EVERY registered UiRoot window (vitals, toolbar,
inventory, spellbook, radar, even the Options panel itself), but
retail's ChatInterface::SetDefaultOpacity/SetActiveOpacity are only
ever called by gmMainChatUI/gmFloatyChatUI — the mechanism is chat-only
in retail, not a global window-opacity feature.
Fix: scope the controller's catch-up loop, OnWindowRegistered,
ReapplyAll, and Dispose to WindowNames.Chat/ChatWindow1-4 only; every
other registered window now stays fully opaque regardless of slider
position, matching retail's own scope. Regressed by
RetailWindowOpacityControllerTests.
OpacityFade_AppliesOnlyToChatWindows_NeverOtherPanels (registers
vitals/toolbar/chat/a floating chat window and asserts the non-chat
windows never move off 1.0 while chat windows still track Default/
Active correctly).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Root cause: DatWidgetFactory builds Config-tab Type-0x10000038 menu
leaves as bare UiMenu instances (matching the vendor/chat channel menu
pattern), but unlike those two controllers, ConfigOptionsPageController
never wired the menu's sprite/font/geometry properties after Bind — so
every dropdown rendered as plain text with no button well, no arrow
cap, and opened no popup on click (#374's fix only corrected click
ROUTING, not the missing chrome).
Fix: ConfigOptionsPageController.ApplyMenuChrome wires every Config-tab
menu row with the SAME retail sprite ids VendorUiController/
ChatWindowController's channel menu already use for this shared popup
catalog (LayoutDesc 0x21000043), verified against the live DAT via
OptionsPanelLiveMountProbeTests' ProbeConfigMenuChrome/
ProbeConfigMenuPopupChrome probes. Regressed by
ConfigOptionsPageControllerTests.MenuRow_SoundFeatures_OpensAndSelects
ThroughRealHitPath_UsingAuthoredPopupGeometry, which drives the real
click-to-open + item-pick path through the authored popup geometry.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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>
Campaign OP gate 2: the screen opened as a visual mess (textless
buttons/tabs, buttons above the window, overlapping text) while the
fixture conformance suite stayed green — the #372 class again. Two root
causes, both proven by the new env-gated live-DAT probe before fixing:
1. MountKeyboardConfig's main LayoutImporter.Build was the ONE mount in
RetailUiRuntime not passing strings.Resolve — every AUTHORED caption
(OK/Cancel/Defaults/Revert/Load/Save, the six ActionClass tab labels,
the Command/Mapping column headers) built empty, while the
controller's own resolveString row captions worked, which is why the
screen was recognizable but textless. Fixed by passing the resolver
like every sibling mount.
2. gmKeyboardUI authors its ListBox row templates (header 0x1000002E,
action row 0x1000002F with the three 100x32 key buttons) as TOP-LEVEL
siblings referenced by dat property 0x64. Retail never instantiates
template-list elements as live widgets (AddItemFromTemplateList
clones from the desc — the same re-import UiTemplateListBox's
TemplateResolver performs), but ImportInfos built them parked at the
screen's (0,0): three key buttons at y=0..32 ABOVE the framed panel
(top y=62) — the 'outside the window' buttons — under a 570x40
header text overlapping them and the top chrome. ImportInfos now
skips top-level elements referenced by a SAME-LAYOUT template list
(the same skip class as the existing BaseElement-prototype filter;
same-layout only because element ids collide across layouts —
0x10000211 is a page in BOTH the options and keyboard layouts).
The probe (ACDREAM_PROBE_LIVE_MOUNT=1) pins both against the real DATs:
prototypes absent from the built tree, and Defaults/Revert/OK/Cancel
resolving on the resolver-passing build. Post-fix the import collapses
to the framed 600x476 panel with every screen button inside its bounds.
Full Release suite: 13,082 passed / 4 skipped / 0 failed.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Campaign OP gate 2 root cause: UiElement.HitTest walks siblings
front-to-back by z-order, so an OPEN UiMenu's extended button+popup
hit-test union was never consulted when a LATER sibling's rect overlapped
the popup area — on the Config tab every dropdown has rows below it, so
Resolution-item clicks toggled the Full Screen / VSync rows underneath
(the gate session's persisted fullscreen/vsync flips were exactly those
stolen clicks). Latent since UiMenu existed; vendor/chat menus only
worked by z-order luck.
Fix: UiMenu's open/close now registers with UiRoot (SetActivePopup /
ClearActivePopup); a registered popup gets FIRST claim on mouse-down,
scroll, and hover routing; a press outside a live popup dismisses it and
is SWALLOWED (the dismissing click must not act on what sat underneath);
hidden/detached owners self-heal the registration on the next pointer
event. UiMenu gains the IsOpen seam and a single SetOpen writer.
Also in this commit, from the same investigation:
- SilkRuntimeDisplayWindowTarget.Apply documents the fullscreen half
honestly: IViewProperties.VideoMode is READ-ONLY, so a resolution pick
while fullscreen cannot switch the display mode through Silk's
abstract API — split out as #376 (native glfwSetWindowMonitor port)
rather than half-shipping untested native interop at a gate tail.
- Gate script §OP6 step 8 re-scoped: test resolution in WINDOWED mode.
Regressed by tests/AcDream.App.Tests/UI/UiMenuPopupRoutingTests.cs —
4 tests driving the real UiRoot input path on a mounted overlapping
tree, with an in-test overlap CONTROL click so the popup assertions
cannot pass vacuously (the #372 lesson: only mount+drive-input tests
catch this class; every fixture-conformance test stayed green through
this bug).
Full Release suite: 13,081 passed / 4 skipped / 0 failed.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The campaign's /goal stop condition is reached: all nine slices landed and
reviewed (OP1/OP2/OP7/OP9 CLOSED; OP3-OP6 + OP8 code-complete with
connected gates owed), and the gate script is the complete per-tab
connected-gate contract (launch with ACDREAM_RETAIL_UI=1; §OP7 already
PASSED live bot-vs-ACE). OP9's ledger row records the combined review
chain (289bf5bc APPROVE-WITH-FIXES -> residuals 07f2b3f7) including
SF-4's corrected test-delta arithmetic (-84, not the implementation
commit's '-80 exactly'). CLAUDE.md Current-state gains the campaign
paragraph per feedback_claude_md_staleness; the settings digest
(claude-memory/project_settings_options_digest.md) is the new domain
entry point.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Combined mechanism-faithfulness x regression/blast-radius pass over
371197a3 (OP9 retirement of the dead F11 settings surface +
GameplaySettings), per the OP5/OP7 single-reviewer precedent for
closeout-shaped slices.
Retirement verified correct: zero production readers of GameplaySettings
or SettingsVM existed pre-commit (checked against the pre-commit tree,
not the diff), SetUiLocked first-call/repeat-call behavior is provably
unchanged by deleting _uiLockConverged, the wire paths
(SetAcceptLootPermits 0x0005, ToggleUiLock, PlayerDescription
convergence) are untouched, the settings.json unknown-key carry-forward
is real, and the AP-196 register edit reconciles (143 -> 142 active,
29 -> 30 retired, total unchanged).
MUST-FIX 1: SaveAudio -> ApplyAudio (OP6 live-apply, live consumer in
ConfigOptionsPageController) lost its only assertion when
SettingsViewModelSavePreservesSectionAndTargetOrder was deleted.
SHOULD-FIX 2-5: stale architecture-doc seam naming SettingsVM; three
dead residues (uncallable private SaveCharacter, orphaned
IngressShutdownRoots.Settings, writerless CharacterSettings path);
test delta enumerates to -84, not the claimed -80 "exactly"; dangling
comment referencing the deleted assertion. Two nits.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Brings b4edee97 (slice), b1968ce9 (M1/M2/M3 rework), f1d50207 (round-2
residuals). Review chain: REJECT -> rework -> REOPEN-narrow -> coordinator
third round; findings docs 2026-08-11-op8-review.md / -op8-rereview.md.
The merge lands OP8's six ListBoxes on top of 057d8cd7's #372 viewport
fix, which auto-heals the blank-pages hazard the re-review flagged — the
OP8 connected gate was contracted to run post-merge for exactly this.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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>
All three MUST verified mechanically: the one-pair-per-action capture
claim PROVEN against RetailDefaults() (9 multi-binding actions, no
counterexample); the identity map now injective (all 122 entries
checked); the confirm dialog real, N-way, accept-only, with the lazy
read's null unreachable in production. The OK/Cancel gesture divergence
is honestly retired (no distinguishing retail affordance = no Risk
symptom to write).
Coordinator third-round residuals: R1 — S2's deferral is commit-message-
only and M2 worsened it (0x6's DAT defaults are the arrow keys, so
Defaults + the flat conflict universe makes 'Move Forward -> Up' trip a
false N-way confirm; the gate script carries no warning). R2 — AP-203's
row body doesn't cover the ten same-verb-sibling-live rows. SHOULD: the
ten Camera Alternate rows render BLANK (retail shows the arrow keys);
identity-map injectivity is load-bearing for M1+M2 with nothing
enforcing it. MERGE NOTE: textually clean vs the campaign tip, but
OP8's six ListBoxes sit on the #372 viewport path — blank on the branch,
auto-healed by merging onto 057d8cd7+; the OP8 gate runs POST-merge.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The 13 ID_ChatOption_TextFilter_* labels (and their _Desc tooltips) live
in string table 0x2300000D, not the 0x23000003 options table the section
headers and slider labels use. Dat-verified: the targeted sweep missed
(0x23000001-0A), the control key resolved ('Auto Target' — machinery
fine), and the exhaustive all-tables sweep (ProbeFilterLabelHome, now a
permanent env-gated probe) hit exactly once: 0x2300000D -> 'Combat'. The
initializer decomp confirms the hash KEYS are the symbol names verbatim
(the vftable-member operands at 0x006f04cd are the known pooled-string
artifact); only the research doc's table attribution was inferred rather
than dat-verified — corrected in §8.
All 13 rows now render their captions instead of the honest-fallback
blanks the first connected gate saw. Full Release suite green (one
Core.Net loss-simulation timing flake on the first run, green targeted
and on rerun).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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>
M1: SetForAction rebuilds bindings as new Binding(chord, action),
defaulting Press/InputScope.Game and dropping the read chord's
ActivationType/InputScope — MovementWalkMode (Hold), the three melee
heights (Hold+MeleeCombat), Missile/Magic-scoped rows, and
CameraInstantMouseLook (Hold, Device 1) all collapse; RestoreDefaultValue
invokes _apply so ONE Defaults click flattens ~140 actions and OK
persists it (gate step 12 tells the user to click Defaults). M2: the ten
camera 0x5/0x6 rows alias one InputAction each — 20 rows over 10 targets,
twins seed identically, clobber each other, self-conflict; the
round-trip test justifies the dual map on independence the mechanism
makes impossible. M3: silent auto-reassign (NotifyReassigned -> empty,
DisplaySystemMessage skips empty) destroys a binding with zero feedback
while the doc claims it reports; retail confirms first
(OpenOverwriteBindingDialog), the plan gate says 'conflict prompt', the
tested SettingsVM prompt precedent exists, AP-204 concedes
RetailDialogFactory is wired, and a fake asserts a message production
contradicts. SHOULD: non-bindable check ordered after first-match
conflict (inverts retail's any-non-bindable-refuses); ActionMap.
ConflictingMaps never read (false conflicts on legitimately-shared
combat keys); Save lacks the writer's try/catch; sparse-row Mapping-3
lands on Mapping-1; ~330 uncached layout imports at startup.
Clean: the 0x26000000 DID (better-anchored than cited — .actionmap
MasterDBMap range, 0x27 is the type tag); no-Clear-button confirmed
against the fixture; six-box grouping matches lane D; identity
byte-comparison is a permanent test; modal capture real;
AP-202/203/204 accurate; OP3 INERT retired. Brief-premise correction:
ff577641 is the CAMPAIGN tip (claude/latest-commits-cb0c8f), not main's
(852cdda7); OP8 additive, only OptionPageModel.cs is a triple-append
merge watch-point.
Rework round 1 to the isolated op8-keyboard worktree.
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>
The re-reviewer byte-decoded ALL six caption sites independently (no
transposition; a third evidence line from the fixture's authored
left/right label geometry), re-verified the audio chain through the one
place an inversion could still hide (the checkbox pass-throughs), and
hand-traced the 97-key conformance table (exactly 97, none invented or
dropped). Residuals applied here: SF-1 the AudioSettings doc comment's
wrong function attribution (the SetDefaultValue literals live in
gmConfigUI::InitOptions @0x0049E435/E457/E479, not InitUIPreferences);
SF-2 the gate script no longer asserts an unread DAT caption — it gates
on behaviour and asks the tester to report the authored English
verbatim; lane A's stale '24 option rows' corrected to 27 (the 39-item
pin is the authoritative tally). AudioSettings.cs change is
comment-only (no executable-code delta; suite state carries from
67b0815c, re-verified at the next code commit).
Campaign state: OP1-OP7 ALL CODE-COMPLETE; OP3/OP4/OP5/OP6 gates ready;
OP8 (Configure Keyboard) is the sole remaining implementation slice.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
R1: UiRoot now delivers WM_CAPTURECHANGED (0x215 — retail's own Win32
event-id space) to the element losing pointer capture on BOTH release
and re-target; UiScrollbar terminates a mid-drag gesture there,
completing it (one DragCompleted flush persisting the user's last-seen
value) and unlatching IsDragging — a panel-close keybind mid-drag or a
second-button re-target can no longer latch the drag flag forever and
silently suppress every later settings flush. Normal MouseUp paths
no-op (the latch is already clear when capture releases).
R2: the scalar latch arms BEFORE the track-click jump applies, so the
jump's own ScalarChanged tick defers its flush to the MouseUp's single
DragCompleted — one flush per press gesture, never the
inline-then-completed double; the DragCompleted doc now states the real
contract (fires once per value-capable gesture incl. capture loss)
instead of the refuted never-on-jump claim.
Tests: capture-loss mid-drag (ends + completes once + stray-MouseUp
no-double), no-drag capture-change no-op, bare-track-click
single-completion with the latch observed armed during the jump tick.
Also reconciles the research doc's U4 row to its closure (the six
caption pairs, the BN zero-fold post-mortem) per the OP6 rework's flag.
Full Release suite: 13,128 passed / 4 skipped / 0 failed (one
documented #250-class allocation flake on first run, green in
isolation and on full-suite rerun).
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>
All findings genuinely closed: the own-slider push verified against the
never-clamp invariant (mid-drag no-op by construction); the DragCompleted
single-fire proven at all three MouseUp sites with a STRONGER capture
argument than the fixer's (pointer capture + Reset needing its own
click); the S3 review-correction verified from git (the round-trip test
existed at e71e5a96 — the review was wrong, the commit right).
Coordinator residuals (third-round, mine): R1 — _draggingThumb clears
only at MouseUp, but UiRoot can drop capture without one (panel-close
keybind mid-drag; second-button re-target), latching IsDragging true
forever so Reset/Defaults apply live but never persist; the
UiRoot.PointerCaptureChanged seam exists for the root fix. R2 — a bare
track click sets _draggingThumb unconditionally in OnScalarEvent, so it
double-flushes and refutes the DragCompleted doc's never-fires-on-jump
claim. Five NOTEs recorded incl. the Chat-side de-scoping blind spot.
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>
M1: the 'U4 resolved: zero range captions' claim is the SAME BN zero-fold
artifact lane A caught on AddHeader — the bytes at 0x0049E4C6 load two
string-id globals (ID_Graphics_Value_Hard/_Soft) and all six labelled
sliders decode cleanly (Stiffness Soft/Hard, Adjustment Slow/Fast, FOV
Narrow/Wide, Brightness Dark/Bright, Performance Speed/Detail, Degrade
Close/Far) — U4 closes the OPPOSITE way; OP5's Chat sliders already
ported the same mechanism. M2: the three Sound 'Disabled' toggles are
sense-INVERTED (the INI key names the string; the registered static is
effect_sounds_enabled = 1) — SFX/ambient ship muted for every user
including existing settings.json installs, undoing Campaign A; a test,
AP-199's wording, and gate step 6 all enshrine it. M3: UI_ChatFontFace
ships 1 of retail's 5 authored choices (all five font strings verbatim
in the binary) with default index 2 out of range — a self-inflicted
copy of the genuine LandscapeDrawDistance oddity. S1: the sixth
trailing separator missing (retail builds 39 items); lane A's '24 rows'
is itself stale (27 is right). S2: Screen Brightness overloads Gamma
([-1,1] default 0 into a [0.5,2.0] multiplier defaulting 1.0). S3:
tooltips reach 12/30 rows (TooltipText exists only on UiButton). S4:
the 800x600 default is absent from the hardcoded resolution list. S5:
the tests pin safe seams and miss every risky one (no ApplyAudio value
assertion, no label/choice conformance, no per-row default pin, no
count pin). A large verified-clean catalog is recorded so the rework
does not re-litigate.
Rework round 1 queues behind the OP5 fixer.
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>
The retail port held under independent re-derivation: AddChild takes ONE
64-bit mask (@0x004862A0, the Society-row split-direction confirmation),
both spot-checked filter blocks byte-exact (12/13 rows, Gameplay
fall-through cases), the (0x16,2) enum-map order proven from
GetDIDFromEnum's body, AP-195's LED swap lands on state 6 of the exact
13x13 face child, self-sizing real, fixtures strictly additive (33
files, zero deletions).
M1: the opacity apply closures never refresh their OWN slider — drag
Default only, Reset: the value reverts (windows change) but the thumb
stays; Defaults self-heals only by both-rows luck. S1: SaveChatOpacity
does a full load+WriteAllText per drag TICK (dozens-to-hundreds of
synchronous JSON round-trips per drag; retail batches via the dirty
timer) — mark-dirty + flush on drag-end/Apply/hide. S2: the
whole-row-culling viewport makes a straddling 240/260px block VANISH
(no scissor stack), and the gate script asks the user to report exactly
that as a regression — false-failure generator + missing register row.
S3: no test for chatWindowMainFilter round-trip or the window-0 seed.
N11 (forward, OP6): 0x10000201 is authored under BOTH the Config and
Chat slots and the flat FindElement returns Chat's (last-write-wins);
OP5's linkage test passes through the flat lookup for the wrong reason.
Fix round queues behind the OP6 builder.
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>
MF-1/M1 CLOSED with the blast scenario walked (Reset now restores live
values; UiLocked converges on every seed). MF-2 CLOSED with a
load-bearing fixture check (all nine verb buttons author Ghosted and
lack DAT 0x0B — TrySetRetailState would otherwise short-circuit). M2
store retirement verified to zero remaining references; M3 walked; S2's
.spv hygiene verified by recomputing the compiler hash scheme.
R1 MUST-FIX: the timestamp now prefixes ChatEntry.Text (the BODY), so
six of ten chat kinds render 'Alice says, "13:05:09 hi"' — retail
composes the line FIRST and prepends the timestamp as a separate string
at display time (AddTextToScroll @0x00563c50 receives composed lines;
fprintf("%ls%ls" ts, text) @0x00563e5b). Fix at ChatVM's display
composition; the two new tests pin the defect and must be rewritten.
R2: the seed-event re-read for OPEN panels (Combat vs Character can
disagree until re-shown; a panel open across reconnect reopens the
wrong-direction Reset for that window) — wire the existing
OnCharacterOptionsChanged hook like LockUI or register it. R3: no test
drives the refresh widget push. R4: headless AutoRepeatAttack default
flip false->true unnamed in AP-196.
Coordinator fixes directly (second round) once OP5 frees the tree.
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>
MF-1 converges with blast M1: rows seed once at startup pre-login and
never re-read; retail's own schedule is the fix (UIOption_Checkbox::
SaveCurrentValue @0x004868E0 = m_current = GetValue() live, m_saved =
m_current, on every OnShown — predicted by OP3 review note N2). MF-2:
the OnOptionChanged Apply/Reset ghosting seam (Ghosted 0x0D / Normal,
PostInit runs it once so both start disabled) is wired by NOTHING in
production. The Defaults correction is independently VERIFIED from the
decomp (SetPlayerOption @0x00486e80 sets m_default from
GetDefaultOptionValue; InqDefaultGameplayOptionProperty's only caller is
the Chat tab's slider path — the plan's U1 directive would have made
Defaults restore nothing). Cross-page button collision confirmed from
the fixture (0x100001FC/FD/FE x3). 18 rows traced end-to-end; six header
hashes + 49 label globals hand-verified. SHOULD: culture-dependent
timestamp separator + overclaimed byte-verification; no
OnClick/ToggleBehavior-driven LED test; gate item 8's logout-flush false
failure. NOTEs incl. sky-dome fog and no mid-session echo repaint.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
M1: the Character tab binds at startup, pre-session — rows seed from
constructor words and nothing re-seeds on PlayerDescription; Reset (and
tab-switch, which is OnHidden->Reset) then publishes the STALE baseline
over server truth, and the one-shot UiLocked assignment can permanently
disagree with the radar's polled read. M2: the Combat panel's three LEDs
still write/read the orphaned GameplaySettings copy — decorative,
wire-less, and visibly divergent from the Character tab's rows for the
same PlayerOptions (the CH3 two-writable-copies mode); headless
hardcodes for the same ids filed alongside. M3: ToggleUiLock's
convergence guard (initialized converged, now comparing values from two
different stores) can early-return past ApplyUiLock — one press flips
the wire and radar but not the windows.
S1: AddText is NOT acdream's transcript chokepoint — speech/tells,
emotes, Turbine channels, combat text and @-replies write ChatLog
directly, so the timestamp prefix reaches only ServerMessage/WeenieError
lines and gate step 13's tell/say half cannot pass. S2: sky.vert never
reads the fog-disable param — crisp terrain against a fogged horizon
with the option on. S3/S4: missing register rows for the seven
default-source changes (ViewCombatTarget's observable default flips) and
the orphaned-but-written GameplaySettings; the timestamp format is
culture-dependent where retail strftime is literal. S6: the two changed
consumer files ship without their test files touched.
Clean: scoped per-page searches structurally sound (fixture-verified),
GetOptionBit thread-safe/allocation-free, the run-XOR byte-correct vs
SetHoldRun @0x006b3370 with wire encoding untouched.
Gate steps 3 and 13 cannot pass pre-fix — the OP4 gate must wait for
the fix round.
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>
Evidence appended to the test script: run-1 diff-and-send exact to
contract; mid-run reconnect idempotence; run-3 cross-process persistence
proof (the fresh seed echoed the blob-only SalvageMultiple value);
graceful converged exits; no refusals, no pre-LoginComplete sends.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
R1: the gate script no longer promises a timestamp prefix on the Magic
macro lines — acdream renders no chat timestamps yet (the Display
Timestamps consumer is OP4 scope; no chat-log file exists, TS-69). A
bare light-blue transcript line is the CORRECT gate outcome.
R2: IsGrounded yields null (silent) for a NULL controller in player
mode — the prior pattern returned false and fired the mid-air refusal
retail cannot produce in that state; comments now match the code.
R3: the dormant-ActivePageChanged pin now applies the real stimulus —
every authored tab button on a dormant host must carry NO click handler
(RetailTabBinding.SetClick never ran), which is AD-73's actual dormancy
mechanism; SwitchTo deliberately has no guard.
OP3 is CLOSED: dual APPROVE-WITH-FIXES -> fix round 386076af ->
re-review REOPEN(narrow) -> this pass. Connected gate now READY.
Full Release suite: 12,956 passed / 4 skipped / 0 failed.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
TWO work products share this commit (a staged-index collision between the
coordinator's docs commit and the OP7 fixer's staged files — content
verified complete and coherent; only this message was wrong before the
amend):
1. OP7 review fixes (all nine findings from
docs/research/2026-08-11-op7-review.md):
- M1: HeadlessSessionDescriptor is a record; WithAccount uses 'with' non-destructive record copy,
so a future property cannot be silently dropped; direct-CLI
regression test proves CharacterOptions survives --user/--password.
- M2 root fix: LiveSessionEventRouter skips BOTH Replace and the
options notification on a trailer-truncated PlayerDescription — a
truncated re-seed can no longer install zeroed words under an armed
latch for OP7's automation to flush into 0x01A1.
- SF1: schema keys validate as ordinal strings against the allowed
names (numeric / comma-combined aliases rejected). SF2: both-true
fellowship exclusion rejected at load, naming both keys. SF3: the
onLoginCompleteSent observer moved after transit.EndTeleport().
SF4: production-hook coverage for all three LoginComplete sites.
SF5: test-script OP7 wire expectation corrected (batched ids ride
only the 0x01A1).
2. docs/research/2026-08-11-op3-rereview.md — OP3 re-review verdict
REOPEN (narrow): M1 byte-decode independently re-verified (6a 07 at
all six sites); residuals R1 (gate script promises a timestamp prefix
acdream doesn't render), R2 (null-controller player-mode still
refuses), R3 (dormancy pin lacks stimulus) — coordinator fixes follow.
Full Release suite at this tree: 12,956 passed / 4 skipped / 0 failed.
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>
The 26-name allow-list cross-checks clean against research §5.2,
CharacterOptionTable, and retail's PlayerOption enum — all three risky
renames verified right (ToggleRun IS PlayerOption 0x0A run-as-default,
not the keybind latch). #368 honored; existing configs byte-identical;
per-route seeder leak-free; the Runtime observation hook never fires in
the graphical host.
MUST-FIX 1: HeadlessProcessHost.WithAccount hand-copies six of seven
descriptor fields — CharacterOptions is dropped, so the K3 direct-CLI
launch mode silently no-ops the whole feature. MUST-FIX 2: a truncated
PlayerDescription RE-seed re-opens OP1's wipe class — Replace installs
zeroed words while the latch stays armed from the earlier complete seed,
and OP7's automation then flushes those zeros into 0x01A1; fix at the
seam (a truncated parse installs nothing, notifies no one). SHOULD:
validate schema names as strings (Enum.TryParse accepts numeric and
comma-combined keys that alias into the allow-list); reject the
contradictory fellowship pair at load (declared both-true oscillates
against retail's mutual exclusion); move the mid-teleport hook to the
sequence tail; cover the three production LoginComplete hooks with the
real controller argument; correct the OP7 test-script line expecting a
0x0005 for batched SalvageMultiple (only the 0x01A1 carries it).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Adds an optional, strict `characterOptions` block to the headless bot config
(D8): keys are exactly the lane-B tier-1 (22) + tier-2 (4) bot-declarable
CharacterOptionId enum-member spellings; an unknown/out-of-tier name fails
config load naming the offending key, before it can ever reach the wire.
HeadlessCharacterOptionsSeeder diffs declared-vs-actual once both of ACE's
real preconditions are known true — GameActionLoginComplete sent (the
FirstEnterWorldDone gate SetCharacterOptions 0x01A1 needs) and a real
PlayerDescription has seeded RuntimeCharacterOptionsState
(HasServerSeed) — learned from whichever of two hooks lands second. Every
differing id routes through OP1's shared IRuntimeCharacterCommands seam:
auto-save ids send SetSingleOption (0x0005) immediately; batched ids also
call SetSingleOption (which only dirties the module) followed by exactly
one SaveOptions flush after the whole declared set has been walked.
Idempotent on reconnect by construction — no dedupe latch, the diff simply
finds nothing once the server agrees.
RuntimeLiveEntitySessionController gains a passive onLoginCompleteSent
observation hook (additive only, never changes when/whether it sends) so
the headless host can learn ACE's gate opened from any of its own two
internal send sites; the third site (direct first-entry completion) is
already owned by HeadlessSessionHost itself. All wiring is synchronous
delegate calls on Runtime's one dedicated update thread — no new
async/Task continuation, honoring #368.
Tests: schema (valid parse, unknown/tier-3 name rejected naming the key,
non-bool rejected, empty/absent no-op), the diff engine against a fake
IRuntimeCharacterCommands (nothing-to-send, auto-save-only, batched-with-
flush, mixed ordering, reconnect idempotence), and two wiring integration
tests — one dispatching a real PlayerDescription game event end-to-end to
a captured wire action, one proving the send lands on the same dedicated
thread every Tick runs on. Full solution suite: 12,935 passed / 4 skipped
/ 0 failed (+17 over baseline 12,918/4/0).
No register row: the characterOptions bot-config surface is acdream-
native tooling over retail's own wire mechanisms (both already ported by
OP1), not a retail UI port with a divergence to record.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Converged MUST-FIX: the six mouse-turning macro lines are typed
ClientLocal 0x1A where retail's AddTextToScroll sites pass type 7 (Magic,
light blue — mechanism lens decoded the stray [4]=7 writes at all six BN
call sites); 0x1A additionally routes to the SpewBox where the 4-slot cap
discards two of the six lines before they draw (blast lens). Fix: a typed
message seam, Magic for the macro, ClientLocal retained for the mid-air
refusal and AD-75; byte-verify the type argument during the fix.
Mechanism: the Gameplay page is NOT an OptionPage in retail
(gmGameplayOptionsUI : UIElement_Field, acclient.h:55857) — the
auto-flush-on-visibility acdream gave it flushes the blob at moments
retail would not, and two green tests pin the wrong shape; the
OnOptionChanged enable-gating seam (@0x004F27D0 — Apply/Reset gated,
Defaults never) is missing from the model; the mid-air refusal fires
where retail is silent (non-player-mode/null controller).
Blast: the plan-assigned docked-host (0x21000017) register ruling was
not filed — deemed a divergence, row owed client-wide; Options is the
only shared-geometry panel with ResizeX/four-edge/min-size, which the
shared _mainPanelGeometry silently reverts when siblings show; three
targeted seams lack pins (real-sibling mutual exclusion, DefaultQueueKey,
dormant ActivePageChanged silence); UA/RA legibility flagged for the
gate. Clean: exactly one toolbar button changed (previously ghosted),
F11 same-action since K.1c, no queue key invented, settings additive.
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>
Focused re-review verifies all ten fix dispositions genuinely close their
findings (fellowship order re-derived from OnChanged @0x0059A8E0; the
seed latch confirmed upstream of every one of the three blob call sites;
TS-71 retirement and TS-73 accuracy checked). Residuals owed to a small
coordinator pass, none reopening: R1 closure hoisting defeats the
per-tick allocation guard (move the IsDirty check into the ctor
lambdas); R2 concurrent MarkDirty during a flush is silently erased by
the trailing clear (needs the dirty-generation token); R3 NOTE — a
truncated-trailer PlayerDescription (options words zero) arms the seed
latch, newly wire-reaching via the timer.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Blast lens: the unguarded Type-8 arm re-classes five pre-existing
elements in four shipped panels (vendor backdrop sprite stops drawing;
character sheet + spellbook swallow clicks; IUiDatStateful propagation
severed), the Type-5 guard's premise is factually wrong (ten shipped
elements author non-empty 0x64), and the 27 stale fixtures make the
harness structurally blind to both — the green suite was true but
meaningless. Merge semantics, fixture provenance, hermeticity, and the
children-attached hook all verified clean.
Mechanism lens: readers decode correctly through the canonical
effective-state path; Type 8's real retail class is UIElement_Panel
(Update @0x0046BD00 — switching behavior confirmed faithful); both
claimed structural identities (UIOption_Slider = horizontal scrollbar,
UIOption_Menu = UiMenu shape) CONFIRMED from fixture fingerprints;
U10 closed (0x10000521 is the bitfield row template, consumed by
CreateChildren @0x00485DF0 via AddItemFromTemplateList(0)); TS-72 is
backwards — the toggle math is decomp-confirmed right, IsSet's all-bits
predicate is confirmed wrong (retail Refresh checks ANY mask bit).
Missing: GetChildRecursive resolution, no-0x32 means NO default switch,
SetupTabPageHash's malformed-entry filters.
Rework round 1 follows: UiTabControl/UiTemplateListBox become
UiDatElement subclasses with dormant behavior (controller-activated),
bitfield rows build from the authored template, IsSet goes any-bit,
all 32 fixtures regenerate as the acceptance gate.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Mechanism lens: 53/53 table rows verified four independent ways; golden
vector recomputed byte-for-byte; 15/15 citations resolve. MUST-FIX: TS-71's
deferral rationale asserts a nonexistent obstacle — both hosts already
funnel one Runtime tick seam (LiveSessionController.Tick), so the 480 s
timer + logout flush wire with zero host edits. SHOULD-FIX: port
CPlayerModule::OnChanged cases 2/0x12 (fellowship mutual-exclusion emits a
second 0x0005); add the id-by-id 53-row word/mask pin.
Blast lens: CH3/CH4 seams bit-identical; routes single-write; reset clean;
the blob echo reads canonical J4.3/J4.5 owners (the important negative).
MUST-FIX: SaveOptions before the PlayerDescription seed would flush CLIENT
DEFAULTS over server options — needs a HasServerSeed latch (set by
Replace, required by TryFlush, cleared by ResetSession). SHOULD-FIX: router
test substitutes a fake binding for the production seam; flush callback
runs under _dirtyGate (deadlock with the router gate once the timer
wires); ledger blind to IsDirty; SaveOptions result encoding differs
between adapters; Replace leaves stale dirty state; no cross-check between
PlayerDescriptionParser enums and CharacterOptionTable.
Fix round follows as one consolidated pass.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>