fix(ui): Campaign LA gate round 2 — character-select scales as one authored canvas
Third iteration on the screen, completing AD-98. The previous substitution
stretched only the root BACKGROUND while the child widgets stayed at their
authored 800x600 pixel positions - and the background painting carries
visual anchors (the World/Characters captions are art), so the user gate
showed captions overlapping the listbox and every widget misaligned
against the stretched art.
Retail model (established at 71bf24fb): fixed-canvas pre-world screens
render at authored 800x600 and the whole composed frame stretches once at
presentation; the blitter has no stretch mode. Our equivalent now does the
same one stage earlier:
- UiRoot.FixedCanvasSize: while the char-select screen is active, the
retained tree lays out in its authored canvas and Draw scopes a uniform
scale onto TextRenderer.CanvasScale; the mouse entry points apply the
exact inverse so MouseX/MouseY and every hit test live in canvas space.
- TextRenderer.AppendQuad is the single emission chokepoint - sprites,
rects, AND glyphs scale together, including retail-authentic non-uniform
aspect distortion and stretched text. World-space HUD stays native (the
scale resets outside UiRoot.Draw).
- CharacterManagementUiController stops resizing Root to the viewport;
activate/deactivate/dispose set and clear the host canvas.
- UiDatElement returns to retail-pure copy-or-tile; the interim
StretchOwnBackgroundToFill flag is deleted.
- AD-98 updated to describe the completed substitution.
Tests: canvas-scale quad math, inverse input mapping (window click lands
on the canvas-space widget), degenerate-size guards, controller keeps
authored extent + sets/clears the canvas. App suite 5085/6 skips; live-DAT
char-select probes 3/3.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
71bf24fb6f
commit
73041d7015
8 changed files with 277 additions and 106 deletions
|
|
@ -201,6 +201,23 @@ public sealed class TextRenderer : IDisposable
|
|||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Campaign LA gate round 2 (register AD-98): uniform canvas scale applied
|
||||
/// to every emitted quad — sprites, rects, AND glyphs — at the single
|
||||
/// emission chokepoint (<see cref="AppendQuad"/>). Retail renders its
|
||||
/// fixed-canvas pre-world screens (char select's authored 800×600, root
|
||||
/// 0x1000039A, zero edge anchors) at authored size and stretches the whole
|
||||
/// composed frame once at presentation; its UI blitter has no stretch mode
|
||||
/// at all (Graphic::Draw @0x00693b20 is copy-or-tile only). We have no
|
||||
/// present-time frame stretch, so the equivalent lives here: while a
|
||||
/// fixed-canvas screen is active, <see cref="UiRoot"/> sets this for the
|
||||
/// duration of its Draw and everything scales together — including retail's
|
||||
/// characteristic non-uniform aspect distortion and stretched glyphs.
|
||||
/// UVs and colors are untouched. Always reset to One outside UiRoot.Draw
|
||||
/// so the world-space HUD keeps native pixels.
|
||||
/// </summary>
|
||||
internal Vector2 CanvasScale = Vector2.One;
|
||||
|
||||
/// <summary>Begin a HUD pass. Call once per frame before any Draw* calls.</summary>
|
||||
public void Begin(Vector2 screenSize)
|
||||
{
|
||||
|
|
@ -388,10 +405,19 @@ public sealed class TextRenderer : IDisposable
|
|||
return ns;
|
||||
}
|
||||
|
||||
private static void AppendQuad(List<float> buf,
|
||||
private void AppendQuad(List<float> buf,
|
||||
float x, float y, float w, float h,
|
||||
float u0, float v0, float u1, float v1, Vector4 color)
|
||||
{
|
||||
// AD-98 canvas stretch — see CanvasScale's doc comment. Applied after
|
||||
// all canvas-space clipping, so geometry and UVs stay consistent.
|
||||
if (CanvasScale != Vector2.One)
|
||||
{
|
||||
x *= CanvasScale.X;
|
||||
y *= CanvasScale.Y;
|
||||
w *= CanvasScale.X;
|
||||
h *= CanvasScale.Y;
|
||||
}
|
||||
// Two triangles (6 verts). CCW in pixel space is clockwise in NDC
|
||||
// because the vertex shader flips Y, so OpenGL's default front-face
|
||||
// is GL_CCW — we rely on cull-face being disabled during HUD pass.
|
||||
|
|
|
|||
|
|
@ -38,6 +38,7 @@ internal sealed class CharacterManagementUiController : IDisposable
|
|||
private readonly List<UiButton> _rows = [];
|
||||
private readonly Dictionary<UiButton, uint> _rowIds = [];
|
||||
|
||||
private Vector2 _authoredCanvas;
|
||||
private RuntimeGenerationToken _lastGeneration;
|
||||
private long _lastRevision = long.MinValue;
|
||||
private uint _deleteDialogContext;
|
||||
|
|
@ -74,24 +75,22 @@ internal sealed class CharacterManagementUiController : IDisposable
|
|||
|
||||
Root.Left = 0f;
|
||||
Root.Top = 0f;
|
||||
Root.Anchors = AnchorEdges.Left | AnchorEdges.Top
|
||||
| AnchorEdges.Right | AnchorEdges.Bottom;
|
||||
if (host.Width > 0f)
|
||||
Root.Width = host.Width;
|
||||
if (host.Height > 0f)
|
||||
Root.Height = host.Height;
|
||||
Root.ClickThrough = false;
|
||||
Root.Visible = false;
|
||||
|
||||
// Campaign LA gate round 2: this root is resized to the live viewport just
|
||||
// above, which is bigger than its authored 800x600 canvas at almost every
|
||||
// real resolution. Its own DirectState background (RenderSurface 0x06007576)
|
||||
// must scale to fill that resized rect, not tile — see
|
||||
// UiDatElement.StretchOwnBackgroundToFill's doc comment for the retail
|
||||
// mechanism (a fixed-canvas screen stretched once at presentation) this
|
||||
// substitutes.
|
||||
if (Root is UiDatElement rootBackground)
|
||||
rootBackground.StretchOwnBackgroundToFill = true;
|
||||
// Campaign LA gate round 2 (register AD-98): the root KEEPS its authored
|
||||
// 800×600 extent — retail never resizes it (zero edge anchors, verified
|
||||
// against the installed DAT) and its blitter has no stretch mode; the
|
||||
// whole composed screen stretches once at presentation. Our equivalent:
|
||||
// while this screen is active, the host stretches the ENTIRE canvas —
|
||||
// widgets, glyphs, and the painted background (which carries the
|
||||
// "World"/"Characters" captions as art) — as one unit via
|
||||
// UiRoot.FixedCanvasSize. Resizing the root here instead is exactly the
|
||||
// half-substitution that misaligned the widgets against the stretched
|
||||
// art at the 2026-08-15 user gate.
|
||||
_authoredCanvas = new Vector2(
|
||||
Root.Width > 0f ? Root.Width : 800f,
|
||||
Root.Height > 0f ? Root.Height : 600f);
|
||||
|
||||
// Create Character belongs to a future campaign. Keep retail's
|
||||
// authored control in place and visibly ghosted; do not hide it or
|
||||
|
|
@ -246,6 +245,7 @@ internal sealed class CharacterManagementUiController : IDisposable
|
|||
{
|
||||
_active = true;
|
||||
Root.Visible = true;
|
||||
_host.FixedCanvasSize = _authoredCanvas;
|
||||
_host.BringToFront(Root);
|
||||
}
|
||||
|
||||
|
|
@ -310,6 +310,7 @@ internal sealed class CharacterManagementUiController : IDisposable
|
|||
}
|
||||
finally
|
||||
{
|
||||
_host.FixedCanvasSize = null;
|
||||
_enter.OnClick = null;
|
||||
_delete.OnClick = null;
|
||||
_restore.OnClick = null;
|
||||
|
|
@ -665,6 +666,7 @@ internal sealed class CharacterManagementUiController : IDisposable
|
|||
{
|
||||
_active = false;
|
||||
Root.Visible = false;
|
||||
_host.FixedCanvasSize = null;
|
||||
}
|
||||
foreach (UiButton row in _rows)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -206,11 +206,9 @@ public class UiDatElement : UiElement, IUiDatStateful
|
|||
public uint? RuntimeImageTexture { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// When true, this element's OWN active-state background media draws as ONE quad
|
||||
/// stretched to exactly fill <see cref="UiElement.Width"/>/<see cref="UiElement.Height"/>
|
||||
/// (UV span 0,0 .. 1,1) instead of the native-pixel TILE formula every other
|
||||
/// <see cref="UiDatElement"/> uses. Default false — every ordinary dat chrome/
|
||||
/// container element (corners, edges, drag bars, tab backdrops) keeps tiling.
|
||||
/// Retail background-blit ground truth (Campaign LA gate round 2, register
|
||||
/// AD-98). Every element draws its own media with the native-pixel TILE
|
||||
/// formula below — retail has no per-element stretch, and neither do we.
|
||||
///
|
||||
/// <para>
|
||||
/// <b>Campaign LA gate round 2 (issue found in the live client: the LA8
|
||||
|
|
@ -247,21 +245,16 @@ public class UiDatElement : UiElement, IUiDatStateful
|
|||
/// </para>
|
||||
///
|
||||
/// <para>
|
||||
/// acdream has no offscreen fixed-resolution UI render target / present-time scale
|
||||
/// pass — <see cref="AcDream.App.UI.Layout.CharacterManagementUiController"/> instead
|
||||
/// resizes the MOUNTED ROOT ELEMENT itself to the live viewport (see its
|
||||
/// constructor) so the screen still fills the window. This flag is the acknowledged
|
||||
/// divergence for that substitution (register row: acdream resizes the element,
|
||||
/// retail stretches the presented frame) — it makes the resized ROOT's own
|
||||
/// background draw as one stretched quad so the VISUAL RESULT matches retail's
|
||||
/// present-time stretch (no tiling) even though the MECHANISM differs. Set only on
|
||||
/// a screen-level mounted root, never on an ordinary descendant/chrome element —
|
||||
/// those keep the native tile formula, which IS what retail's own blit does for
|
||||
/// content that lives inside the (in retail) fixed 800x600 canvas.
|
||||
/// acdream's equivalent of that present-time stretch is
|
||||
/// <see cref="AcDream.App.UI.UiRoot.FixedCanvasSize"/>: while a fixed-canvas
|
||||
/// screen (char select) is active, the WHOLE retained tree — this tile draw
|
||||
/// included — is scaled uniformly at the renderer's quad chokepoint, with the
|
||||
/// inverse applied to mouse input. Elements therefore keep their authored
|
||||
/// canvas-space sizes here, and the tile formula stays exactly retail's:
|
||||
/// inside the authored canvas an element never exceeds its media's native
|
||||
/// span unless retail itself tiled it.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
public bool StretchOwnBackgroundToFill { get; set; }
|
||||
|
||||
protected override void OnDraw(UiRenderContext ctx)
|
||||
{
|
||||
if (MediaVisible && RuntimeImageTexture is uint runtimeTexture)
|
||||
|
|
@ -290,23 +283,14 @@ public class UiDatElement : UiElement, IUiDatStateful
|
|||
var (tex, tw, th) = _resolve(file);
|
||||
if (tex != 0 && tw != 0 && th != 0)
|
||||
{
|
||||
if (StretchOwnBackgroundToFill)
|
||||
{
|
||||
// One quad, UV 0..1 — see StretchOwnBackgroundToFill's doc comment
|
||||
// for the retail mechanism this substitutes (a fixed-canvas screen
|
||||
// stretched once at presentation).
|
||||
ctx.DrawSprite(tex, 0, 0, Width, Height, 0, 0, 1, 1, Vector4.One);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Normal → TILE at native size on both axes (UV-repeat; GL_REPEAT-wrapped
|
||||
// UI texture) — retail's Graphic::Draw/Graphic::PutImage (0x00693b20/
|
||||
// 0x00693a30) copy-or-tile blit; see StretchOwnBackgroundToFill's doc
|
||||
// comment for the corrected citation (NOT ImgTex::TileCSI, which is
|
||||
// land-surface-only). Overlay/Alphablend use the same blit (the sprite
|
||||
// shader already alpha-blends). No Stretch mode exists in DrawModeType.
|
||||
ctx.DrawSprite(tex, 0, 0, Width, Height, 0, 0, Width / tw, Height / th, Vector4.One);
|
||||
}
|
||||
// TILE at native size on both axes (UV-repeat; GL_REPEAT-wrapped
|
||||
// UI texture) — retail's Graphic::Draw/Graphic::PutImage
|
||||
// (0x00693b20/0x00693a30) copy-or-tile blit; NOT ImgTex::TileCSI,
|
||||
// which is land-surface-only (corrected citation, see the class
|
||||
// doc). Overlay/Alphablend use the same blit (the sprite shader
|
||||
// already alpha-blends). No Stretch mode exists in DrawModeType;
|
||||
// whole-canvas stretching happens at UiRoot.FixedCanvasSize.
|
||||
ctx.DrawSprite(tex, 0, 0, Width, Height, 0, 0, Width / tw, Height / th, Vector4.One);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -32,6 +32,32 @@ public sealed class UiRoot : UiElement
|
|||
/// <summary>Single owner for named retained-window lifecycle and raise policy.</summary>
|
||||
public RetailWindowManager WindowManager { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Campaign LA gate round 2 (register AD-98): when set, the retained tree
|
||||
/// is laid out in this fixed authored canvas (the char-select screen's
|
||||
/// 800×600) and the whole tree — widgets, glyphs, art — is stretched to
|
||||
/// the window as one unit, matching retail's present-time frame stretch
|
||||
/// for fixed-canvas pre-world screens. Draw applies the scale at the
|
||||
/// renderer's quad chokepoint; the mouse entry points apply the inverse,
|
||||
/// so <see cref="MouseX"/>/<see cref="MouseY"/> and every hit test live
|
||||
/// in canvas space. Null (the in-world default) is native 1:1.
|
||||
/// </summary>
|
||||
public Vector2? FixedCanvasSize { get; set; }
|
||||
|
||||
/// <summary>Window→canvas stretch factor; One when no fixed canvas is set.</summary>
|
||||
public Vector2 CanvasScale =>
|
||||
FixedCanvasSize is { X: > 0f, Y: > 0f } canvas && Width > 0f && Height > 0f
|
||||
? new Vector2(Width / canvas.X, Height / canvas.Y)
|
||||
: Vector2.One;
|
||||
|
||||
private (int x, int y) MapWindowToCanvas(int x, int y)
|
||||
{
|
||||
Vector2 scale = CanvasScale;
|
||||
return scale == Vector2.One
|
||||
? (x, y)
|
||||
: ((int)MathF.Round(x / scale.X), (int)MathF.Round(y / scale.Y));
|
||||
}
|
||||
|
||||
// ── Device-level state ───────────────────────────────────────────────
|
||||
public int MouseX { get; private set; }
|
||||
public int MouseY { get; private set; }
|
||||
|
|
@ -370,6 +396,21 @@ public sealed class UiRoot : UiElement
|
|||
}
|
||||
|
||||
public void Draw(UiRenderContext ctx)
|
||||
{
|
||||
// AD-98 fixed-canvas stretch: scope the renderer's canvas scale to
|
||||
// exactly this tree's draws (world-space HUD stays native).
|
||||
ctx.TextRenderer.CanvasScale = CanvasScale;
|
||||
try
|
||||
{
|
||||
DrawCore(ctx);
|
||||
}
|
||||
finally
|
||||
{
|
||||
ctx.TextRenderer.CanvasScale = Vector2.One;
|
||||
}
|
||||
}
|
||||
|
||||
private void DrawCore(UiRenderContext ctx)
|
||||
{
|
||||
// Render children (panels) sorted by z-order — modal last so it
|
||||
// sits on top.
|
||||
|
|
@ -401,6 +442,7 @@ public sealed class UiRoot : UiElement
|
|||
|
||||
public void OnMouseMove(int x, int y)
|
||||
{
|
||||
(x, y) = MapWindowToCanvas(x, y);
|
||||
int dx = x - MouseX;
|
||||
int dy = y - MouseY;
|
||||
MouseX = x;
|
||||
|
|
@ -552,6 +594,7 @@ public sealed class UiRoot : UiElement
|
|||
|
||||
public void OnMouseDown(UiMouseButton btn, int x, int y, uint flags = 0)
|
||||
{
|
||||
(x, y) = MapWindowToCanvas(x, y);
|
||||
MouseX = x; MouseY = y;
|
||||
UpdateButtonFlag(btn, down: true);
|
||||
_pressX = x; _pressY = y;
|
||||
|
|
@ -707,6 +750,7 @@ public sealed class UiRoot : UiElement
|
|||
|
||||
public void OnMouseUp(UiMouseButton btn, int x, int y, uint flags = 0)
|
||||
{
|
||||
(x, y) = MapWindowToCanvas(x, y);
|
||||
MouseX = x; MouseY = y;
|
||||
UpdateButtonFlag(btn, down: false);
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue