using System; using System.Linq; using AcDream.App.UI; namespace AcDream.App.UI.Layout; /// /// Hybrid factory: behavioral element Types map to dedicated widgets (verbatim /// algorithm ports); everything else (and unknown Types) falls back to /// . /// /// /// Type 12 elements that carry NO own state media (pure style prototypes / /// BaseElement stores) return null from and are skipped. /// Type 12 elements that DO carry own sprites (e.g. a chat element whose Type-0 /// derived form inherited Type 12 from its base prototype) are rendered normally. /// See docs/research/2026-06-15-layoutdesc-format.md Correction 8. /// /// /// /// The meter's back/front 3-slice sprite ids live on grandchild image elements, /// NOT on the meter element itself (format doc §11). /// walks two layers down to extract them: the two Type-3 container children /// ordered by (back behind = lower, front /// on top = higher), then within each container the image children that carry /// a DirectState ("" key) sprite, ordered by their X position to obtain /// left-cap / center-tile / right-cap. /// /// /// /// The expand-detail overlay present in the front container carries ONLY named /// states ("HideDetail"/"ShowDetail") — no "" DirectState entry — so the /// TryGetValue("") filter in excludes it /// automatically. /// /// public static class DatWidgetFactory { /// /// Creates the for , sets its /// rect (Left/Top/Width/Height) and Anchors, and returns it. /// /// Resolved, merged element snapshot from the LayoutDesc importer. /// RenderSurface id → (GL tex handle, pixel width, pixel height). /// Returns (0,0,0) when the texture is not yet uploaded. /// Retail UI font for the meter's "cur/max" number overlay. /// May be null pre-load — the meter falls back to the debug bitmap font. /// The widget, or null for a pure Type-12 style prototype with no own sprites (caller skips it). public static UiElement? Create(ElementInfo info, Func resolve, UiDatFont? datFont) { // Type 12 = style prototype / BaseElement store referenced by BaseLayoutId. // PURE prototypes (no own state media) are property bags — never rendered; skip them. // A Type-12 element that carries its own state media (e.g. a chat Send button whose // Type-0 derived element inherited Type 12 from its base prototype) has sprites to // show and must render. See format doc §8 and the G1 task note. if (info.Type == 12 && info.StateMedia.Count == 0) return null; UiElement e = info.Type switch { 1 => new UiButton(info, resolve), // UIElement_Button (reg :125828) 7 => BuildMeter(info, resolve, datFont), // UIElement_Meter 11 => new UiScrollbar(), // UIElement_Scrollbar (reg :124137) _ => new UiDatElement(info, resolve), // generic fallback for all other types }; // Propagate position + size (pixel-exact from the dat). e.Left = info.X; e.Top = info.Y; e.Width = info.Width; e.Height = info.Height; // Honor the dat's draw order so overlapping pieces (grip overlay over bevel chrome) layer correctly. e.ZOrder = (int)info.ReadOrder; // Map the four raw edge-anchor values to the AnchorEdges bit-flag that the // UI layout engine uses for reflow. e.Anchors = ElementReader.ToAnchors(info.Left, info.Top, info.Right, info.Bottom); return e; } // ── Meter ──────────────────────────────────────────────────────────────── /// /// Builds a and populates its six 3-slice sprite ids by /// reading the meter's grandchild image elements (format doc §11). /// /// /// Structure the importer produces for each meter (UIElement_Meter): /// /// meter (Type 7) /// ├── back-layer container (Type 3, lower ReadOrder — drawn first / behind) /// │ ├── left-cap image (DirectState "" → File = back-left sprite) /// │ ├── center image (DirectState "" → File = back-tile sprite) /// │ └── right-cap image (DirectState "" → File = back-right sprite) /// ├── front-layer container (Type 3, higher ReadOrder — drawn on top) /// │ ├── left-cap image (→ front-left sprite) /// │ ├── center image (→ front-tile sprite) /// │ ├── right-cap image (→ front-right sprite) /// │ └── expand overlay (named "ShowDetail"/"HideDetail" only — NO DirectState — IGNORED) /// └── text label (Type 0) (IGNORED — Fill/Label providers bound by VitalsController in Task 6) /// /// /// /// /// and are NOT set here. /// They are bound to the live stat providers in Task 6 (VitalsController). /// /// private static UiMeter BuildMeter(ElementInfo info, Func resolve, UiDatFont? datFont) { var m = new UiMeter { SpriteResolve = resolve, DatFont = datFont, }; // The two 3-slice containers are Type-3 children of the meter element. // ReadOrder determines draw order: the back track has a LOWER ReadOrder // (drawn first, behind the fill), the front has a HIGHER ReadOrder (on top). var containers = info.Children .Where(c => c.Type == 3) .OrderBy(c => c.ReadOrder) .ToList(); if (containers.Count != 2) Console.WriteLine($"[D.2b] meter 0x{info.Id:X8}: {containers.Count} Type-3 slice containers (expected 2) — bars may render as solid-color fallback."); if (containers.Count >= 1) { var (l, t, r) = SliceIds(containers[0]); m.BackLeft = l; m.BackTile = t; m.BackRight = r; } if (containers.Count >= 2) { var (l, t, r) = SliceIds(containers[1]); m.FrontLeft = l; m.FrontTile = t; m.FrontRight = r; } return m; } /// /// Returns the (left, tile, right) sprite ids for a 3-slice container, /// extracting them from the container's image children that carry a DirectState /// ("" key) with a non-zero file id, ordered left-to-right by their X position. /// /// /// Children that carry ONLY named states (e.g. the expand-detail overlay with /// "ShowDetail"/"HideDetail" entries but no "" key) are excluded automatically /// because for "" returns /// false. /// /// private static (uint left, uint tile, uint right) SliceIds(ElementInfo container) { // Only children that have a non-zero DirectState image are slice candidates. // The expand-detail overlay has NO DirectState entry, so it's excluded here. // Project the File during filtering to avoid a second TryGetValue lookup. // Stable sort: on an X tie, original Children insertion order (dat key-sort order) wins. var slices = container.Children .Where(c => c.StateMedia.TryGetValue("", out var med) && med.File != 0) .Select(c => (c.X, File: c.StateMedia[""].File)) .OrderBy(t => t.X) .ToList(); uint left = slices.Count > 0 ? slices[0].File : 0u; uint tile = slices.Count > 1 ? slices[1].File : 0u; uint right = slices.Count > 2 ? slices[2].File : 0u; return (left, tile, right); } }