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>
130 lines
4.3 KiB
C#
130 lines
4.3 KiB
C#
using System.Numerics;
|
||
using AcDream.App.UI;
|
||
using AcDream.App.UI.Layout;
|
||
using Xunit;
|
||
|
||
namespace AcDream.App.Tests.UI;
|
||
|
||
/// <summary>
|
||
/// Campaign LA gate round 2 (register AD-98): retail renders fixed-canvas
|
||
/// pre-world screens (char select, authored 800×600) at authored size and
|
||
/// stretches the whole composed frame at presentation — its UI blitter has no
|
||
/// stretch mode. acdream's equivalent is <see cref="UiRoot.FixedCanvasSize"/>:
|
||
/// draw applies one uniform scale at the renderer's quad chokepoint, and the
|
||
/// mouse entry points apply the exact inverse so hit-testing lives in canvas
|
||
/// space. These tests pin the scale math and the inverse input mapping — the
|
||
/// half that, if wrong, makes the user click on art and hit nothing.
|
||
/// </summary>
|
||
public class UiRootFixedCanvasTests
|
||
{
|
||
[Fact]
|
||
public void CanvasScale_IsOne_WithoutFixedCanvas()
|
||
{
|
||
var root = new UiRoot { Width = 1920f, Height = 1080f };
|
||
Assert.Equal(Vector2.One, root.CanvasScale);
|
||
}
|
||
|
||
[Fact]
|
||
public void CanvasScale_IsWindowOverCanvas_WhenFixed()
|
||
{
|
||
var root = new UiRoot
|
||
{
|
||
Width = 1920f,
|
||
Height = 1080f,
|
||
FixedCanvasSize = new Vector2(800f, 600f),
|
||
};
|
||
Assert.Equal(new Vector2(2.4f, 1.8f), root.CanvasScale);
|
||
}
|
||
|
||
[Fact]
|
||
public void CanvasScale_IsOne_ForDegenerateCanvasOrWindow()
|
||
{
|
||
var zeroCanvas = new UiRoot
|
||
{
|
||
Width = 1920f,
|
||
Height = 1080f,
|
||
FixedCanvasSize = new Vector2(0f, 600f),
|
||
};
|
||
Assert.Equal(Vector2.One, zeroCanvas.CanvasScale);
|
||
|
||
var zeroWindow = new UiRoot
|
||
{
|
||
Width = 0f,
|
||
Height = 0f,
|
||
FixedCanvasSize = new Vector2(800f, 600f),
|
||
};
|
||
Assert.Equal(Vector2.One, zeroWindow.CanvasScale);
|
||
}
|
||
|
||
/// <summary>
|
||
/// The load-bearing inverse: a click at WINDOW coordinates must land on the
|
||
/// widget whose authored CANVAS rect the user visually clicked. The button
|
||
/// sits at canvas (300,400)+(120×40); at a 1920×1080 window over an 800×600
|
||
/// canvas it appears at window (720,720)-(1008,792). Clicking window
|
||
/// (860,750) — canvas (358,417) — must click it; clicking window (300,400)
|
||
/// — canvas (125,222), visually empty — must not.
|
||
/// </summary>
|
||
[Fact]
|
||
public void MouseInput_MapsWindowCoordsToCanvasSpace()
|
||
{
|
||
var root = new UiRoot
|
||
{
|
||
Width = 1920f,
|
||
Height = 1080f,
|
||
FixedCanvasSize = new Vector2(800f, 600f),
|
||
};
|
||
int clicks = 0;
|
||
var button = new UiButton(
|
||
new ElementInfo { Width = 120, Height = 40 },
|
||
_ => (0u, 0, 0))
|
||
{
|
||
Left = 300f,
|
||
Top = 400f,
|
||
Width = 120f,
|
||
Height = 40f,
|
||
OnClick = () => clicks++,
|
||
};
|
||
root.AddChild(button);
|
||
|
||
// Truncation, not rounding (batch review F6): 860/2.4 = 358.33 → 358,
|
||
// 750/1.8 = 416.67 → 416. Rounding mapped the window's far edge one
|
||
// past the canvas's last valid coordinate (1919 → 800), losing a 1px
|
||
// hit-test band at the right/bottom.
|
||
root.OnMouseDown(UiMouseButton.Left, 860, 750);
|
||
root.OnMouseUp(UiMouseButton.Left, 860, 750);
|
||
Assert.Equal(1, clicks);
|
||
Assert.Equal(358, root.MouseX);
|
||
Assert.Equal(416, root.MouseY);
|
||
|
||
// The far window edge maps INSIDE the canvas.
|
||
root.OnMouseMove(1919, 1079);
|
||
Assert.Equal(799, root.MouseX);
|
||
Assert.Equal(599, root.MouseY);
|
||
|
||
root.OnMouseDown(UiMouseButton.Left, 300, 400);
|
||
root.OnMouseUp(UiMouseButton.Left, 300, 400);
|
||
Assert.Equal(1, clicks);
|
||
}
|
||
|
||
[Fact]
|
||
public void MouseInput_IsUntouched_WithoutFixedCanvas()
|
||
{
|
||
var root = new UiRoot { Width = 1920f, Height = 1080f };
|
||
int clicks = 0;
|
||
var button = new UiButton(
|
||
new ElementInfo { Width = 120, Height = 40 },
|
||
_ => (0u, 0, 0))
|
||
{
|
||
Left = 300f,
|
||
Top = 400f,
|
||
Width = 120f,
|
||
Height = 40f,
|
||
OnClick = () => clicks++,
|
||
};
|
||
root.AddChild(button);
|
||
|
||
root.OnMouseDown(UiMouseButton.Left, 360, 420);
|
||
root.OnMouseUp(UiMouseButton.Left, 360, 420);
|
||
Assert.Equal(1, clicks);
|
||
}
|
||
}
|