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); } } }