fix(ui): button property 0x0D was never "disabled", and it killed every Journal button

Reported symptom: Abandon, New, Record, Start, First and Last all unclickable.
That Abandon was in the list is what identified it — Abandon is deliberately
unwired, so if it behaved the same as the others the cause could not be wiring.

UiButton read authored property 0x0D as "starts disabled" (Enabled = !0x0D).
It was the one property read in that file with no citation, and it was wrong.
Every button on the Journal panel authors 0x0D, so every one built disabled:
visible, because drawing never consults Enabled, and unclickable, because
UiElement.HitTest skips disabled elements. Exactly the reported shape.

The evidence is a sweep of every installed layout (LayoutDump gained --ghosted
for it): 85 elements author 0x0D and ALL 85 author it TRUE — not one False
anywhere in the client — and no panel ever clears it, the only four
SetAttribute_Bool(.., 0xd, ..) sites in the binary being chargen appearance,
the keymap option and the barber. A flag that is only ever true, never cleared,
and sits on New, Record, Start, Delete and Reset cannot mean "dead button";
under the old reading 85 elements were permanently dead in a shipping game.

It is not a pure ghosted LOOK either, which is why this ignores it rather than
moving it to appearance: the same 85 mix live buttons with inert column headers
("Contract", "Status", "Title", "Timer", "Label", "#"), and one appearance
cannot be right for both. Registered as QJ-2 with the measurement, so the open
question is recorded rather than quietly decided.

The test that asserted the old behaviour carried no citation either — it
encoded the same assumption. It now asserts the evidenced behaviour, with a
companion test proving the state machine's own Ghosted transition still
suppresses a click: that mechanism is separate and did not change.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Erik 2026-08-21 15:58:03 +02:00
parent c5cc8ae5fc
commit 73a04244e7
4 changed files with 84 additions and 4 deletions

View file

@ -506,7 +506,6 @@ public sealed class UiButton : UiElement, IUiGlobalTimeListener, IUiDatStateful
? repeatInterval
: 0f;
_selected = info.TryGetEffectiveBool(0x0Eu, out bool selected) && selected;
bool disabled = info.TryGetEffectiveBool(0x0Du, out bool ghosted) && ghosted;
// State defaulting matches UiDatElement exactly:
// DefaultStateName wins; else "Normal" if that state has a sprite; else DirectState ("").
@ -516,7 +515,25 @@ public sealed class UiButton : UiElement, IUiGlobalTimeListener, IUiDatStateful
ActiveState = "Normal";
// else ActiveState stays "" (DirectState)
Enabled = !disabled;
// Property 0x0D was previously read here as "starts disabled"
// (Enabled = !0x0D). That was the one uncited property read in this
// file, and it was wrong. Measured across every installed layout
// (LayoutDump --ghosted): 85 elements author 0x0D and ALL 85 author it
// TRUE — not one False anywhere in the client. No panel ever clears it
// either: the only four SetAttribute_Bool(…, 0xd, …) sites in the
// binary are chargen appearance, the keymap option and the barber, and
// gmContractsUI/gmJournalUI never call SetAttribute at all.
//
// A flag that is only ever authored true, never cleared, and sits on
// elements that must be clickable — New, Record, Start, Delete, Reset,
// the search box — cannot mean "this button is dead". Reading it that
// way disabled every button on the Journal panel: visible, because
// drawing does not consult Enabled, but unclickable, because
// UiElement.HitTest skips disabled elements.
//
// Retail's UIElement_Button::UpdateState_ @0x00471CF0 uses 0x0D to
// pick the Ghosted VISUAL state, which the state machine below already
// models through ActiveState. Input is not its business.
FaceWidth = mediaInfo?.Width ?? info.Width;
FaceHeight = mediaInfo?.Height ?? info.Height;
UpdateVisualState();