fix(ui,runtime): Campaign LA gate-round-2 batch-review fixes F1,F3-F8; file #401

F1 (MUST-FIX): RetailWaitDialogView was the ONE dialog view the 0a7dc7d6
EffectiveCanvasSize sweep missed - the Entering World wait dialog (fires
on ENTER, the char screen primary action) still centered against the raw
window and landed off the visible canvas. Same three-line fix as its three
siblings; the enter-wait test now grows the window over the fixed canvas
and asserts canvas-space centering.

F3/F4: two stale assertions about the DELETED first AD-98 substitution
(the register section-2 header line and the live-DAT oracle test doc) now
describe the completed FixedCanvasSize mechanism - the C4-closeout failure
mode, caught before it cost anything.

F5: RetailDialogData.Confirmation sets ElementAttribute40 itself (retail
MakeConfirmExitDialog writes 0x8E=1, 0xAC=1, 0xC5); the manual set in
GameplayConfirmationController is gone.

F6: MapWindowToCanvas truncates instead of rounding - rounding mapped the
window far edge one past the canvas last valid coordinate, a 1px dead
hit-test band; test updated to truncation semantics + far-edge case.

F7: AD-98 records that the no-letterbox aspect claim has no decomp
citation and is confirmed by the user live gate pass 2026-08-15.

F8: the durable world-name read in StartCore is IsCurrent-gated like every
neighbouring step.

F2 filed as #401 (invert RetailUi to opt-out - product-default decision,
not a gate fix).

App 5100+6 skips, Runtime 1666, green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Erik 2026-08-15 12:28:06 +02:00
parent 36c14902a8
commit 0baebce262
10 changed files with 87 additions and 19 deletions

View file

@ -58,8 +58,9 @@ public sealed class GameplayConfirmationController : IDisposable
? request.Message + " Continue?"
: _composeMessage?.Invoke(request.Type, request.Message)
?? request.Message;
var data = RetailDialogData.Confirmation(message)
.Set(RetailDialogProperty.ElementAttribute40, true);
// ElementAttribute40 now comes from RetailDialogData.Confirmation
// itself (batch review F5 — retail's confirmation builders all set it).
var data = RetailDialogData.Confirmation(message);
_dialogContext = _dialogs.MakeDialog(data);
return _dialogContext != 0u;
}

View file

@ -108,11 +108,16 @@ public sealed class RetailDialogData
return clone;
}
/// <summary>Type-1 confirmation data. Sets element attribute 0x40 — retail's
/// own confirmation builders do (e.g. <c>MakeConfirmExitDialog @0x004ed250</c>
/// writes 0x8E=1, 0xAC=1, 0xC5=message), same as the Wait/TextInput factories
/// below (gate-round-2 batch review F5).</summary>
public static RetailDialogData Confirmation(string message)
{
ArgumentNullException.ThrowIfNull(message);
return new RetailDialogData()
.Set(RetailDialogProperty.Type, RetailDialogType.Confirmation)
.Set(RetailDialogProperty.ElementAttribute40, true)
.Set(RetailDialogProperty.Message, message);
}

View file

@ -95,10 +95,15 @@ internal sealed class RetailWaitDialogView : IRetailDialogView
private void SizeAndCenter()
{
// Center against the layout space (fixed canvas while a pre-world
// screen is active) — gate-round-2 batch review F1: this was the ONE
// dialog view the 0a7dc7d6 sweep missed, and it fires on ENTER (the
// char screen's primary action), centering off the visible canvas.
var space = _host.EffectiveCanvasSize;
Root.Left = 0f;
Root.Top = 0f;
Root.Width = _host.Width;
Root.Height = _host.Height;
Root.Width = space.X;
Root.Height = space.Y;
_popup.Left = MathF.Round((Root.Width - _popup.Width) * 0.5f);
_popup.Top = MathF.Round((Root.Height - _popup.Height) * 0.5f);
}

View file

@ -65,10 +65,14 @@ public sealed class UiRoot : UiElement
private (int x, int y) MapWindowToCanvas(int x, int y)
{
// Truncate, not round (batch review F6): rounding maps the window's
// last column/row one past the canvas's last valid coordinate
// (1919/2.4 → 800, past 799), creating a 1px dead band at the far
// right/bottom edge. Truncation maps 0..1919 onto 0..799 exactly.
Vector2 scale = CanvasScale;
return scale == Vector2.One
? (x, y)
: ((int)MathF.Round(x / scale.X), (int)MathF.Round(y / scale.Y));
: ((int)(x / scale.X), (int)(y / scale.Y));
}
// ── Device-level state ───────────────────────────────────────────────

View file

@ -768,7 +768,11 @@ public sealed class LiveSessionController
// ServerNameReceived subscription exists — so the durable field is
// read here exactly like the roster above (gate-round-2 live fix:
// the event-only wiring left the World box empty against ACE).
if (_operations.GetServerInfo(session) is { } serverInfo)
// IsCurrent-gated like every neighbouring step (batch review F8):
// a generation flip here must not write the old server's name
// into the new generation's state.
if (IsCurrent(scope, generation)
&& _operations.GetServerInfo(session) is { } serverInfo)
CharacterSelectionState.ApplyWorldName(serverInfo.WorldName);
// Campaign LA slice LA2: the probe short-circuit lands here —