using System;
using System.Numerics;
namespace AcDream.App.UI.Layout;
///
/// Generic dat element: draws its active state's media by DrawMode (Normal=tile,
/// Alphablend/Overlay=blended overlay). The fallback renderer for every element type
/// without a dedicated behavioral widget (chrome corners/edges, drag bars, resize grips);
/// faithful because retail's base element render is exactly "stamp the media per draw-mode".
///
///
/// For Plan 1, all observed draw modes produce the same alpha-blended tiled quad — the
/// sprite shader already alpha-blends, so no per-mode branch is needed here. The named
/// constants document the real enum for Plan 2.
///
///
///
/// DrawModeType (DatReaderWriter.Enums), stored as int in to
/// keep this dat-free. See docs/research/2026-06-15-layoutdesc-format.md §6:
/// Undefined=0, Normal=1, Overlay=2, Alphablend=3. There is no Stretch mode.
///
///
///
/// Tiling uses UV-repeat on BOTH axes (Width/tw, Height/th) so vertical
/// chrome edges (e.g. a 5×10 sprite drawn over a 5×48 rect) tile vertically too.
/// sets
/// GL_REPEAT on both S and T, so vertical tiling is always active.
///
///
///
/// OP2 rework (2026-08-11): unsealed so behavioral Type-8/Type-5 widgets
/// (, ) can subclass it and stay
/// DORMANT by default — every element of those two dat Types that reaches
/// gets this exact media-draw / ClickThrough /
/// behavior unless a controller explicitly opts a specific
/// instance into its behavioral mechanism. This is what makes the unconditional Type-8/
/// Type-5 factory mappings safe for the pre-existing shipped panels the OP2 REJECT
/// findings named (`docs/research/2026-08-11-op2-review-blast.md`,
/// `docs/research/2026-08-11-op2-review-mechanism.md`).
///
///
public class UiDatElement : UiElement, IUiDatStateful
{
// DrawModeType enum values from DatReaderWriter.Enums.
// See docs/research/2026-06-15-layoutdesc-format.md §6.
#pragma warning disable IDE0051 // private constants kept for documentation / Plan 2
private const int DrawUndefined = 0;
private const int DrawNormal = 1;
private const int DrawOverlay = 2;
private const int DrawAlphablend = 3;
#pragma warning restore IDE0051
/// Protected so subclasses (e.g. ,
/// ) can read authored state/media for their own
/// mechanism-specific fields without a second copy of the merged snapshot.
protected readonly ElementInfo Info;
private readonly Func _resolve;
/// The dat element id from . Exposed so controllers
/// can identify which logical element a UiDatElement represents when walking subtrees
/// (e.g. footer state groups that appear once per tab page but share the same dat id).
public uint ElementId => Info.Id;
/// Which state name to render. "" = the unnamed DirectState.
/// Falls back to DirectState if the named state is absent.
public string ActiveState { get; set; } = "";
public uint ActiveRetailStateId
{
get
{
if (string.IsNullOrEmpty(ActiveState))
return UiStateInfo.DirectStateId;
foreach (var (id, state) in Info.States)
if (string.Equals(state.Name, ActiveState, StringComparison.Ordinal))
return id;
return UiButtonStateMachine.TryStateId(ActiveState, out uint standard)
? standard
: RetailUiStateIds.TryStateId(ActiveState, out uint custom) ? custom : 0u;
}
}
public override string ActiveCursorStateName => ActiveState;
public bool TrySetRetailState(uint stateId)
{
UiStateInfo? selectedState = null;
if (stateId == UiStateInfo.DirectStateId)
{
if (!Info.States.TryGetValue(stateId, out selectedState)
&& !Info.StateMedia.ContainsKey(""))
return false;
ActiveState = "";
}
else if (Info.States.TryGetValue(stateId, out selectedState))
{
ActiveState = selectedState.Name;
}
else
{
string stateName = UiButtonStateMachine.StateName(stateId);
if (string.IsNullOrEmpty(stateName))
stateName = RetailUiStateIds.StateName(stateId);
if (string.IsNullOrEmpty(stateName) || !Info.StateMedia.ContainsKey(stateName))
return false;
ActiveState = stateName;
}
if (selectedState?.PassToChildren == true)
{
foreach (UiElement child in Children)
if (child is IUiDatStateful stateful)
stateful.TrySetRetailState(stateId);
}
return true;
}
/// Merged for this element.
/// Dat file-id → (GL texture handle, native px width, native px height).
/// Returns (0,0,0) when the texture is not yet uploaded.
public UiDatElement(ElementInfo info, Func resolve)
{
Info = info;
_resolve = resolve;
ClickThrough = true; // generic decoration; behavioral widgets opt back in
// Pick the initial active state: retail applies DefaultState when set; falls back
// to "Normal" when the element has a Normal-state sprite (retail's implicit default
// for stateful elements like tabs and buttons); else the unnamed DirectState ("").
if (!string.IsNullOrEmpty(info.DefaultStateName))
ActiveState = info.DefaultStateName;
else if (info.StateMedia.ContainsKey("Normal"))
ActiveState = "Normal";
// else ActiveState stays "" (DirectState)
// Outline 0x21 / OutlineColor 0x22 from the effective-default state, mirroring
// DatWidgetFactory.BuildText's seed of UiText (round-5 review S2).
Outline = info.Outline;
if (info.OutlineColor.HasValue)
OutlineColor = info.OutlineColor.Value;
}
///
/// Returns the (File, DrawMode) for the current ,
/// falling back to the DirectState ("" key) if the named state is absent.
/// Returns (0, 0) if neither exists.
///
// exposed for unit testing
public (uint File, int DrawMode) ActiveMedia()
=> Info.StateMedia.TryGetValue(ActiveState, out var m) ? m
: Info.StateMedia.TryGetValue("", out var d) ? d
: (0u, 0);
/// Optional click handler. Set by a controller for interactive dat
/// elements (e.g. the chat Send / max-min buttons). Requires
/// = false to receive click events.
public Action? OnClick { get; set; }
public Action? OnClickAt { get; set; }
public override bool HandlesClick => OnClick is not null || OnClickAt is not null;
public override bool OnEvent(in UiEvent e)
{
if (e.Type == UiEventType.Click && (OnClick is not null || OnClickAt is not null))
{
OnClick?.Invoke();
OnClickAt?.Invoke(e.Data1, e.Data2);
return true;
}
return false;
}
/// Optional centered text label drawn over the sprite (e.g. the "Send"
/// button face whose dat sprite is a blank frame). Null = sprite only.
public string? Label { get; set; }
/// Dat font for . Required for the label to draw.
public UiDatFont? LabelFont { get; set; }
/// Label color (default white).
public Vector4 LabelColor { get; set; } = Vector4.One;
///
/// Campaign CC gate round 1 closeout (Group 1, R2-5): per-instance
/// multiplicative sprite tint, threaded into both
/// calls this class makes (the runtime-image path and the ordinary
/// authored-media path) — same shape and same default-identity
/// no-op-for-existing-callers guarantee as .
///
public Vector4 Tint { get; set; } = Vector4.One;
/// Retail LayoutDesc property 0x21 (two-pass glyph outline,
/// UIElement_Text::SetOutline @0x0046a81c). Seeded in the ctor from the
/// element's effective-default state, same as
/// (round-5 review S2 — per-STATE switching is AP-192).
public bool Outline { get; set; }
/// Retail LayoutDesc property 0x22 (m_curOutlineColor,
/// ctor default black). Only meaningful when is true.
public Vector4 OutlineColor { get; set; } = UiRenderContext.DefaultOutlineColor;
///
/// Controls only this element's authored media. Descendants still draw.
/// Layered retail composites use this to place a template's chrome behind a
/// viewport while retaining its text descendants above that viewport.
///
public bool MediaVisible { get; set; } = true;
///
/// Runtime image installed on this region in place of its authored state media.
/// A non-null value mirrors retail UIRegion::ClearImage followed by
/// UIRegion::SetImage; zero deliberately leaves the region image-less.
/// The image remains this element's own media, so authored descendants retain
/// their normal foreground relationship.
///
public uint? RuntimeImageTexture { get; set; }
///
/// 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.
///
///
/// Campaign LA gate round 2 (issue found in the live client: the LA8
/// character-select background repeated across the window instead of scaling
/// with it). Retail's generic UI sprite blit —
/// Graphic::Draw (acclient 0x00693b20) dispatching to
/// Graphic::PutImage (0x00693a30) for an exact/undersized destination, or a
/// modulo-wrapped tile loop otherwise — has exactly two behaviors, copy or tile;
/// it can never scale a source image up to a larger destination. This is confirmed
/// against two candidate "draw-mode" fields that could have carried a stretch bit
/// and don't: BlitMode (acclient.h ~line 3135 — Blit_Normal/3Alpha/4Alpha/
/// Colorize/Multiply/Screen/Grayscale/NOP are all COLOR-BLEND selectors) and
/// MD_Data_Image::m_drawMode/DrawModeType (Undefined/Normal/Overlay/
/// Alphablend — also a blend selector; the "Normal → tile" reading in
/// docs/research/2026-06-15-layoutdesc-format.md §6 cited
/// ImgTex::TileCSI (0x0053e740), but that function is exclusively called from
/// TexMerge::CopyAndTile/ImgTex::CopyCSI for LAND-SURFACE terrain
/// texture compositing (TerrainTex) — never from the UI element system; the
/// citation was a coincidental name match, not the real call site).
///
///
///
/// The LA8 root itself (0x1000039A) authors LeftEdge=TopEdge=RightEdge=BottomEdge=0
/// ("no anchor" — confirmed against the installed DAT via
/// CharacterManagementLiveDatTests.RootAuthorsNoEdgeAnchors_RetailNeverResizesItSelf),
/// so retail's own UIElement::UpdateForParentSizeChange (0x00462640) never
/// touches this element's size at all — it stays a fixed 800x600 rect. The only way
/// retail's whole pre-world "flow" scene (background AND buttons AND listbox
/// together — "the background scales with the root") can still fill an arbitrary
/// window resolution edge-to-edge, with the generic sprite blit only ever able to
/// copy-or-tile, is that these screens render into a fixed, authored-size (800x600)
/// target and the WHOLE FRAME is stretched once at presentation — a step entirely
/// outside the UIRegion/Graphic::Draw sprite system.
///
///
///
/// acdream's equivalent of that present-time stretch is
/// : 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.
///
///
protected override void OnDraw(UiRenderContext ctx)
{
if (MediaVisible && RuntimeImageTexture is uint runtimeTexture)
{
if (runtimeTexture != 0u)
{
ctx.DrawSprite(
runtimeTexture,
0f,
0f,
Width,
Height,
0f,
0f,
1f,
1f,
Tint);
}
DrawLabel(ctx);
return;
}
var (file, _) = ActiveMedia();
if (MediaVisible && file != 0)
{
var (tex, tw, th) = _resolve(file);
if (tex != 0 && tw != 0 && th != 0)
{
// 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, Tint);
}
}
DrawLabel(ctx);
}
private void DrawLabel(UiRenderContext ctx)
{
// Centered text label over the sprite (retail draws button captions as text;
// their dat sprites are blank frames).
if (Label is { Length: > 0 } label && LabelFont is { } lf)
{
float tx = (Width - lf.MeasureWidth(label)) * 0.5f;
float ty = (Height - lf.LineHeight) * 0.5f;
ctx.DrawStringDat(lf, label, tx, ty, LabelColor, Outline, OutlineColor);
}
}
}