diff --git a/src/AcDream.App/UI/Layout/RetailConfirmationDialogView.cs b/src/AcDream.App/UI/Layout/RetailConfirmationDialogView.cs
index 71cc4673..d21dbddc 100644
--- a/src/AcDream.App/UI/Layout/RetailConfirmationDialogView.cs
+++ b/src/AcDream.App/UI/Layout/RetailConfirmationDialogView.cs
@@ -129,10 +129,15 @@ internal sealed class RetailConfirmationDialogView : IRetailDialogView
private void SizeAndCenter()
{
+ // Center against the space the tree lays out in — the fixed authored
+ // canvas while a pre-world screen is active (gate round 2: centering
+ // against the raw window width put the exit dialog far right of the
+ // stretched 800x600 canvas center).
+ 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);
}
diff --git a/src/AcDream.App/UI/Layout/RetailConfirmationTextInputDialogView.cs b/src/AcDream.App/UI/Layout/RetailConfirmationTextInputDialogView.cs
index e21d330b..1b1467c8 100644
--- a/src/AcDream.App/UI/Layout/RetailConfirmationTextInputDialogView.cs
+++ b/src/AcDream.App/UI/Layout/RetailConfirmationTextInputDialogView.cs
@@ -150,10 +150,13 @@ internal sealed class RetailConfirmationTextInputDialogView : IRetailDialogView
private void SizeAndCenter()
{
+ // Center against the layout space (fixed canvas while a pre-world
+ // screen is active) — see RetailConfirmationDialogView.SizeAndCenter.
+ 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);
}
diff --git a/src/AcDream.App/UI/Layout/RetailMessageDialogView.cs b/src/AcDream.App/UI/Layout/RetailMessageDialogView.cs
index a9246ac5..9ec4d987 100644
--- a/src/AcDream.App/UI/Layout/RetailMessageDialogView.cs
+++ b/src/AcDream.App/UI/Layout/RetailMessageDialogView.cs
@@ -95,10 +95,13 @@ internal sealed class RetailMessageDialogView : IRetailDialogView
private void SizeAndCenter()
{
+ // Center against the layout space (fixed canvas while a pre-world
+ // screen is active) — see RetailConfirmationDialogView.SizeAndCenter.
+ 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);
}
diff --git a/src/AcDream.App/UI/UiRoot.cs b/src/AcDream.App/UI/UiRoot.cs
index fe3246b1..7ca428ac 100644
--- a/src/AcDream.App/UI/UiRoot.cs
+++ b/src/AcDream.App/UI/UiRoot.cs
@@ -44,6 +44,19 @@ public sealed class UiRoot : UiElement
///
public Vector2? FixedCanvasSize { get; set; }
+ ///
+ /// The coordinate space the retained tree currently lays out in: the fixed
+ /// authored canvas while one is active, else the window itself. Anything
+ /// that positions against "the screen" (dialog centering, full-screen
+ /// scrims) must use THIS — the gate-round-2 exit dialog centered against
+ /// the 1920px window while the tree lived in the 800px canvas, landing far
+ /// right of the visible screen center.
+ ///
+ public Vector2 EffectiveCanvasSize =>
+ FixedCanvasSize is { X: > 0f, Y: > 0f } canvas
+ ? canvas
+ : new Vector2(Width, Height);
+
/// Window→canvas stretch factor; One when no fixed canvas is set.
public Vector2 CanvasScale =>
FixedCanvasSize is { X: > 0f, Y: > 0f } canvas && Width > 0f && Height > 0f
diff --git a/src/AcDream.Runtime/Session/LiveSessionController.cs b/src/AcDream.Runtime/Session/LiveSessionController.cs
index 3cdc3413..5973e22c 100644
--- a/src/AcDream.Runtime/Session/LiveSessionController.cs
+++ b/src/AcDream.Runtime/Session/LiveSessionController.cs
@@ -172,6 +172,16 @@ public interface ILiveSessionOperations
WorldSession CreateSession(IPEndPoint endpoint);
void Connect(WorldSession session, string user, string password);
CharacterList.Parsed? GetCharacters(WorldSession session);
+ ///
+ /// Campaign LA gate round 2 (world-name live fix): ACE sends ServerName
+ /// (0xF7E1) in the SAME connect-response batch as CharacterList, so the
+ /// ServerNameReceived event fires during the handshake pump —
+ /// BEFORE the controller's binding subscribes. The durable
+ /// field is therefore read
+ /// synchronously after Connect, exactly like
+ /// reads the durable roster; the event remains for post-connect updates.
+ ///
+ ServerName.Parsed? GetServerInfo(WorldSession session) => session.ServerInfo;
void StartCharacterSelectionReceive(WorldSession session) =>
session.StartCharacterSelectionReceive();
void EnterWorld(WorldSession session, int activeCharacterIndex);
@@ -753,6 +763,14 @@ public sealed class LiveSessionController
return new LiveSessionStartResult(LiveSessionStartStatus.Deferred);
}
+ // World name arrives in the SAME connect batch as CharacterList,
+ // during the handshake pump — before the binding's
+ // 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)
+ CharacterSelectionState.ApplyWorldName(serverInfo.WorldName);
+
// Campaign LA slice LA2: the probe short-circuit lands here —
// only after a real CharacterList was returned and its roster was
// reported, before TrySelectCharacter ever runs. A missing
diff --git a/tests/AcDream.App.Tests/UI/Layout/CharacterManagementUiControllerTests.cs b/tests/AcDream.App.Tests/UI/Layout/CharacterManagementUiControllerTests.cs
index 678540f6..b75345e0 100644
--- a/tests/AcDream.App.Tests/UI/Layout/CharacterManagementUiControllerTests.cs
+++ b/tests/AcDream.App.Tests/UI/Layout/CharacterManagementUiControllerTests.cs
@@ -386,6 +386,13 @@ public sealed class CharacterManagementUiControllerTests
UiButton exit = environment.Button(
CharacterManagementUiController.ExitElementId);
+ // Gate round 2 follow-up: the window is BIGGER than the fixed canvas
+ // (the live shape — 1920×1080 window over the 800×600 authored
+ // screen). The exit dialog centered against the raw window width and
+ // landed far right of the visible canvas center.
+ environment.Host.Width = 1920f;
+ environment.Host.Height = 1080f;
+
exit.OnClick!();
Assert.NotEqual(0u, controller.ConfirmExitDialogContext);
@@ -393,6 +400,14 @@ public sealed class CharacterManagementUiControllerTests
Assert.Equal(
"Are you sure you want to leave?",
Message(dialog));
+ // The dialog's scrim + popup must live in CANVAS space: scrim spans
+ // exactly the canvas, popup centers within it.
+ Assert.Equal(800f, dialog.Root.Width);
+ Assert.Equal(600f, dialog.Root.Height);
+ UiElement popup = Assert.Single(
+ dialog.Root.Children,
+ static child => child.Visible && child.Width > 0f);
+ Assert.InRange(popup.Left + popup.Width * 0.5f, 399f, 401f);
DialogButton(dialog, RetailConfirmationDialogView.RejectButtonId).OnClick!();
Assert.Equal(0, environment.Runtime.RequestExitCalls);
diff --git a/tests/AcDream.Runtime.Tests/Session/LiveSessionControllerTests.cs b/tests/AcDream.Runtime.Tests/Session/LiveSessionControllerTests.cs
index 02d8bee4..b2d752a6 100644
--- a/tests/AcDream.Runtime.Tests/Session/LiveSessionControllerTests.cs
+++ b/tests/AcDream.Runtime.Tests/Session/LiveSessionControllerTests.cs
@@ -113,6 +113,13 @@ public sealed class LiveSessionControllerTests
return Characters;
}
+ /// Gate-round-2 world-name live fix: mirrors the durable
+ /// post-connect read the production path performs (the wire event
+ /// fires during the handshake, before any subscriber exists).
+ public ServerName.Parsed? ServerInfo { get; set; }
+
+ public ServerName.Parsed? GetServerInfo(WorldSession session) => ServerInfo;
+
public void StartCharacterSelectionReceive(WorldSession session) =>
calls.Add("start-character-selection-receive");
@@ -349,6 +356,33 @@ public sealed class LiveSessionControllerTests
Assert.True(host.CommandBuses[0].Active);
}
+ ///
+ /// Gate-round-2 live fix: ACE sends ServerName (0xF7E1) in the SAME
+ /// connect batch as CharacterList, so the event fires during the
+ /// handshake pump before the binding subscribes — the World box stayed
+ /// empty against a live server while the event-only unit tests passed.
+ /// Start must read the durable ServerInfo after connect, exactly like
+ /// the roster.
+ ///
+ [Fact]
+ public void Start_AppliesWorldNameFromDurableServerInfoAfterConnect()
+ {
+ var calls = new List();
+ var operations = new TestOperations(calls)
+ {
+ ServerInfo = new ServerName.Parsed(1, 128, "sawato"),
+ };
+ var host = new TestHost(calls);
+ var controller = new LiveSessionController(operations);
+
+ LiveSessionStartResult result = controller.Start(LiveOptions(), host);
+
+ Assert.Equal(LiveSessionStartStatus.Connected, result.Status);
+ Assert.Equal(
+ "sawato",
+ controller.CharacterSelectionState.View.Snapshot.WorldName);
+ }
+
[Fact]
public void Start_ReportsRosterFromCharacterListBeforeSelection()
{