fix #420: seed face-segment media states so character select stops crashing the client

Every launcher-started play session on 2026-08-19 died a few seconds after
login. The user's own session evidence shows it three times in a row:
started -> connected -> characterList -> exited code 1 "crashed", with
client.err.log carrying

  System.ArgumentNullException: Value cannot be null. (Parameter 'key')
     at System.Collections.Generic.Dictionary`2.FindValue(TKey key)
     at AcDream.App.UI.UiButton.OnDraw(UiRenderContext ctx)

UiButton allocated its per-face-segment media-state array as `new string[n]`,
leaving every element null, while the single-face sibling _faceMediaState was
correctly seeded to "" (DirectState). NextMediaState returns `current`
unchanged on three of its four arms — including retail's own "committed state
authored with an empty media array keeps the previous media playing" rule — so
on a multi-segment button whose committed state carries no media the null
survived the first SyncMediaStates and reached
ElementInfo.StateMedia.TryGetValue(null), throwing mid-paint and taking the
process down.

Seed the array with "" at construction. That is what the constructor's
existing comment already claimed the media machine did ("the media machine
begins on the element's BASE media"); only the segment array was left out.

Verified by reverting the one-line fix: the new regression test throws
ArgumentNullException from UiButton.ActiveFile, the same frame as the live
crash. AcDream.App.Tests UiButton filter: 41 passed, 3 skipped.

Found while investigating Campaign LU item 4 ("launching the selected
character doesn't work") — this is why nothing worked. Also lands the Campaign
LU plan doc, whose recon section records the mechanisms the remaining slices
build on.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Erik 2026-08-19 18:32:28 +02:00
parent a3b0455f59
commit a34e8f2a17
4 changed files with 344 additions and 0 deletions

View file

@ -441,6 +441,19 @@ public sealed class UiButton : UiElement, IUiGlobalTimeListener, IUiDatStateful
// state at construction, exactly retail's Initialize -> SetState
// ordering.
_segmentMediaStates = new string[_faceSegments.Length];
// #420: seed every segment with DirectState (""), exactly like
// _faceMediaState's own initializer above. `new string[n]` leaves
// nulls, and NextMediaState returns `current` UNCHANGED on three of
// its four arms (committed state authored with an empty media array,
// or no committed/base state and no "" entry) — so on a multi-segment
// button whose committed state carries no media the null survived the
// first SyncMediaStates and reached
// ElementInfo.StateMedia.TryGetValue(null), throwing
// ArgumentNullException ("Parameter 'key'") from inside OnDraw.
// Observed live: it killed the client mid-paint on the character-
// select screen on every launch (session status.jsonl: connected ->
// characterList -> exited code 1 "crashed").
Array.Fill(_segmentMediaStates, "");
_resolve = resolve;
ClickThrough = false; // buttons are interactive — opt OUT of click-through