fix #382: chat-window indicator buttons invisible until first hovered
Root cause (found via reference-identity-verified live-DAT probing, not
a guess): the four main-chat-window indicator buttons (0x10000522-
0x10000525) resolve their own correct ActiveState="Normal" at
construction, then get blanked to "" moments later in the SAME
LayoutImporter.Build call. The indicator column's backing panel
(0x10000600) authors PassToChildren=true on its own empty DirectState
(confirmed live: States[0xFFFFFFFF].PassToChildren == true); when
LayoutImporter.BuildWidget's post-attach state reapply runs for that
panel, UiDatElement.TrySetRetailState cascades its DirectStateId to
every IUiDatStateful child, including the already-correctly-resolved
buttons. UiButton.TrySetRetailState's DirectStateId branch used to
accept that cascade because every button structurally carries a
DirectStateId entry in its States dict as a property bag (ToggleBehavior/
RolloverEnabled/etc), independent of whether it authors any blank
sprite, so TryFindState(DirectStateId) found that entry and blanked
ActiveState even with no "" media. A hover "fixed" it only because
UiButtonStateMachine.RequestedState resolves to the same canonical
Normal id regardless of PointerOver when RolloverEnabled is false.
Retail's own decompiled UIElement::SetState @0x00464e70 does the exact
same unconditional-commit-plus-cascade; retail avoids this specific bug
purely through construction timing (UIElement::Initialize's SetState
call precedes child-tree construction, so a cascade fired during import
always iterates zero children). Our port's LayoutImporter.BuildWidget
deliberately reapplies in the opposite order to give retained
PassToChildren tabs their authored child media, so this literal
state-machine port needed a compensating guard.
Fix: UiButton.TrySetRetailState's DirectStateId branch now requires
REAL "" media (HasStateMedia("")) before accepting the transition.
Scoped to UiButton only; UiDatElement's parallel branch and the cascade
mechanism are unchanged, so CharacterStatController's own
PassToChildren-driven chrome children are unaffected. Register row
AP-206 records the divergence from retail's literal unconditional-
commit semantics. Regressed by two fast unit tests in UiButtonTests.cs
(DirectStateCascade_WithoutRealMedia_DoesNotBlankAnAlreadyResolvedState,
DirectStateTransition_WithRealMedia_StillSucceeds) plus a live-mount
probe confirming all four buttons resolve ActiveState="Normal"
immediately after import against the real installed DAT.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
a31fd631ad
commit
d1c60df946
6 changed files with 292 additions and 10 deletions
|
|
@ -26,15 +26,84 @@ What does NOT go here:
|
|||
|
||||
## #382 — Floating chat-window tab buttons are invisible until first hovered
|
||||
|
||||
**Status:** OPEN — filed 2026-08-11 at Campaign OP gate 4 (user report with
|
||||
screenshots). The side chat windows' numbered tab buttons render NOTHING in
|
||||
their resting state — only once the pointer hovers one does it paint its
|
||||
correct orange numbered-button art (the user's third screenshot shows the
|
||||
correct post-hover look). So the un-hovered/Normal state's media is not
|
||||
being drawn (or the button starts in a state with no StateDesc media) while
|
||||
the Highlight state works. Investigate the CH6 chat-window tab buttons'
|
||||
state/sprite wiring at the live mount — evidence first (which state id the
|
||||
button starts in, what media that state resolves) — before fixing.
|
||||
**Status:** ROOT-CAUSED + FIXED (this commit) — pending the user's re-gate.
|
||||
Filed 2026-08-11 at Campaign OP gate 4 (user report: nothing renders at
|
||||
rest on the four main-chat-window indicator buttons — `0x10000522`-
|
||||
`0x10000525`, `ChatWindowController.Indicator1Id`-`Indicator4Id` — hover
|
||||
reveals the correct orange art).
|
||||
|
||||
**A prior session's extensive static trace found no bug and left a
|
||||
live-mount probe for this session** (see the earlier revision of this
|
||||
entry, preserved in git history). Running that probe with reference-
|
||||
identity verification (`RuntimeHelpers.GetHashCode` + `ReferenceEquals`
|
||||
against a build-time-captured instance, not just re-reading a possibly-
|
||||
different widget) found the actual defect: the SAME `UiButton` instance
|
||||
resolves `ActiveState="Normal"` correctly at its own construction, then
|
||||
gets blanked to `""` moments later — still inside the SAME
|
||||
`LayoutImporter.Build` call, before the probe ever reads it.
|
||||
|
||||
**ROOT CAUSE.** The indicator column's backing panel (`0x10000600`)
|
||||
authors `PassToChildren=true` on its OWN empty DirectState (confirmed
|
||||
live: `States[0xFFFFFFFF].PassToChildren == true` — almost certainly
|
||||
intended to route the panel's OTHER named states, HideDetail/ShowDetail,
|
||||
to an unrelated sibling, not Normal/Highlight to these buttons).
|
||||
`LayoutImporter.BuildWidget` reapplies every widget's own default state
|
||||
AFTER its children are attached (so retained PassToChildren TABS get
|
||||
their authored Open/Closed child media — see that method's own comment);
|
||||
when the PANEL's reapply runs, `UiDatElement.TrySetRetailState` cascades
|
||||
its DirectStateId to every `IUiDatStateful` child, including the four
|
||||
ALREADY-correctly-resolved buttons. `UiButton.TrySetRetailState`'s
|
||||
DirectStateId branch used to accept that cascade because
|
||||
`_mediaInfo.States` structurally carries a DirectStateId entry on EVERY
|
||||
button (it is the property bag for ToggleBehavior/RolloverEnabled/etc,
|
||||
independent of whether the button authors any blank sprite — see
|
||||
`UiButtonTests.AddBoolProperty`), so `TryFindState(DirectStateId)` found
|
||||
that entry and blanked `ActiveState` even though `StateMedia` has no `""`
|
||||
key at all. A synthetic hover "fixed" it only because
|
||||
`UiButtonStateMachine.RequestedState` resolves to the SAME canonical
|
||||
Normal id regardless of `PointerOver` when `RolloverEnabled` is false, so
|
||||
the next `UpdateVisualState()` call (from the hover event) re-picks
|
||||
"Normal" from `_availableStates` — the blanking was a one-shot
|
||||
construction-time event, not a persistent state.
|
||||
|
||||
Retail's own decompiled `UIElement::SetState @0x00464e70` does the exact
|
||||
same unconditional-commit-plus-blind-cascade (`ElementDesc::AccessStateDesc`
|
||||
finding ANY StateDesc, media or not, is enough to commit `m_curStateDesc`/
|
||||
`m_state` and cascade to every child when `PassToChildren` is set).
|
||||
Retail avoids this specific bug purely through construction TIMING:
|
||||
`UIElement::Initialize`'s `SetState(m_defaultState)` call is literally the
|
||||
second operation in the function, before any child-tree construction —
|
||||
so a PassToChildren cascade fired during import always iterates ZERO
|
||||
children in retail. Our port's `LayoutImporter.BuildWidget` deliberately
|
||||
reapplies in the opposite order (children built first, then the parent's
|
||||
default is reapplied and cascades DOWN into the now-existing children),
|
||||
which is what makes this literal state-machine port hit a case retail's
|
||||
own timing never exercises.
|
||||
|
||||
**Fix:** `UiButton.TrySetRetailState`'s DirectStateId branch now requires
|
||||
REAL `""` media (`HasStateMedia("")`) before accepting the transition — a
|
||||
structurally-present-but-media-less States entry no longer counts. Scoped
|
||||
to `UiButton` only; `UiDatElement.TrySetRetailState`'s parallel branch and
|
||||
the cascade mechanism itself are unchanged, so `CharacterStatController`'s
|
||||
own three-chrome-children PassToChildren cascade (which depends on the
|
||||
SAME reapply ordering) is unaffected. Register row AP-206 records the
|
||||
divergence from retail's literal unconditional-commit semantics.
|
||||
Regressed by two new fast unit tests in
|
||||
`tests/AcDream.App.Tests/UI/UiButtonTests.cs`
|
||||
(`DirectStateCascade_WithoutRealMedia_DoesNotBlankAnAlreadyResolvedState`,
|
||||
`DirectStateTransition_WithRealMedia_StillSucceeds` — the companion
|
||||
positive case, confirming an AUTHORED blank DirectState can still be
|
||||
entered explicitly) plus a rewritten, now-asserting live-mount probe
|
||||
(`ChatIndicatorButtonLiveMountProbeTests.
|
||||
IndicatorButtons_ResolveNormalStateAtRest_ThroughTheLiveImportPath`,
|
||||
`ACDREAM_PROBE_LIVE_MOUNT=1`) that confirms the fix against the real
|
||||
installed DAT: all four buttons now resolve `ActiveState="Normal"`
|
||||
immediately after import, with no hover required.
|
||||
|
||||
**Re-gate:** the four floating-window indicator buttons (mail/chat-tab
|
||||
style LEDs at the top-left of the main chat window) should show their
|
||||
correct orange numbered art immediately on window open, with no hover
|
||||
needed.
|
||||
|
||||
## #381 — Options-panel footer (Apply/Reset/Defaults) needs an opaque backing field; list content shows through between the buttons
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue